diff --git a/app/javascript/components/bootcamp/DrawingPage/useDrawingEditorHandler.ts b/app/javascript/components/bootcamp/DrawingPage/useDrawingEditorHandler.ts index 8da838d52a..09b4313e88 100644 --- a/app/javascript/components/bootcamp/DrawingPage/useDrawingEditorHandler.ts +++ b/app/javascript/components/bootcamp/DrawingPage/useDrawingEditorHandler.ts @@ -54,7 +54,6 @@ export function useDrawingEditorHandler() { // value is studentCode const evaluated = interpret(value, { externalFunctions: drawExerciseInstance?.availableFunctions, - language: 'JikiScript', }) const { frames } = evaluated diff --git a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/useEditorHandler.tsx b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/useEditorHandler.tsx index ae941c7e5c..0e2528bf18 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/useEditorHandler.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/useEditorHandler.tsx @@ -90,6 +90,7 @@ export function useEditorHandler({ try { runCode(value, editorViewRef.current) } catch (e: unknown) { + console.log(e) setHasUnhandledError(true) setUnhandledErrorBase64( JSON.stringify({ diff --git a/app/javascript/components/bootcamp/SolveExercisePage/exercises/time/DigitalClockExercise.tsx b/app/javascript/components/bootcamp/SolveExercisePage/exercises/time/DigitalClockExercise.tsx index 9723fcb9f7..c5f06f1557 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/exercises/time/DigitalClockExercise.tsx +++ b/app/javascript/components/bootcamp/SolveExercisePage/exercises/time/DigitalClockExercise.tsx @@ -65,7 +65,6 @@ export default class DigitalClockExercise extends Exercise { } public getState() { - console.log(this.displayedTime) return { displayedTime: this.displayedTime } } diff --git a/app/javascript/components/bootcamp/SolveExercisePage/hooks/useConstructRunCode/useConstructRunCode.ts b/app/javascript/components/bootcamp/SolveExercisePage/hooks/useConstructRunCode/useConstructRunCode.ts index c9db9aa55f..056f6a2f35 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/hooks/useConstructRunCode/useConstructRunCode.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/hooks/useConstructRunCode/useConstructRunCode.ts @@ -79,7 +79,6 @@ export function useConstructRunCode({ const context = { externalFunctions: exercise?.availableFunctions, - language: 'JikiScript', languageFeatures: config.interpreterOptions, } // @ts-ignore diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts index 1c03f3143c..f6a39bf442 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/expect.ts @@ -24,13 +24,13 @@ export function expect({ testsType, } return { - toExist() { + toBeDefined() { return { ...returnObject, pass: actual !== undefined && actual !== null, } }, - toNotExist() { + toBeUndefined() { return { ...returnObject, pass: actual === undefined || actual === null, diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts index e5b5f375c0..a0c51203ab 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execGenericTest.ts @@ -12,9 +12,7 @@ export function execGenericTest( const evaluated = evaluateFunction( options.studentCode, - { - language: 'JikiScript', - }, + {}, testData.function, ...params ) diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts index f252412b8a..7c38390fbd 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/execProjectTest.ts @@ -24,7 +24,6 @@ export function execProjectTest( const context = { externalFunctions: exercise.availableFunctions, - language: 'JikiScript', languageFeatures: options.config.interpreterOptions, } let evaluated diff --git a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts index 1b1712e494..a78581b21f 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/test-runner/generateAndRunTestSuite/generateExpects.ts @@ -16,7 +16,7 @@ export function generateExpects( } // These are normal function in/out tests. We always know the actual value at this point -// (as it's returned from the function) so we can just compare it to the expected value. +// (as it's returned from the function) so we can just compare it to the check value. function generateExpectsForIoTests( interpreterResult: InterpretResult, testData: TaskTest, @@ -45,18 +45,18 @@ function generateExpectsForStateTests( // We only need to do this once, so do it outside the loop. const state = exercise.getState() - return testData.checks!.map((expected) => { - const matcher = expected.matcher || 'toEqual' + return testData.checks!.map((check) => { + const matcher = check.matcher || 'toEqual' - // Expected can either be a reference to the final state or a function call. + // Check can either be a reference to the final state or a function call. // We pivot on that to determine the actual value let actual // If it's a function call, we split out any params and then call the function // on the exercise with those params passed in. - if (expected.name.includes('(') && expected.name.endsWith(')')) { - const fnName = expected.name.slice(0, expected.name.indexOf('(')) - const argsString = expected.name.slice(expected.name.indexOf('(') + 1, -1) + if (check.name.includes('(') && check.name.endsWith(')')) { + const fnName = check.name.slice(0, check.name.indexOf('(')) + const argsString = check.name.slice(check.name.indexOf('(') + 1, -1) // We eval the args to turn numbers into numbers, strings into strings, etc. const safe_eval = eval // https://esbuild.github.io/content-types/#direct-eval @@ -73,17 +73,17 @@ function generateExpectsForStateTests( // Our normal state is much easier! We just check the state object that // we've retrieved above via getState() for the variable in question. else { - actual = state[expected.name] + actual = state[check.name] } - const errorHtml = expected.errorHtml?.replaceAll('%actual%', actual) || '' + const errorHtml = check.errorHtml?.replaceAll('%actual%', actual) || '' return expect({ - ...expected, + ...check, testsType: 'state', actual, errorHtml, - name: expected.label ?? expected.name, - })[matcher as AvailableMatchers](expected.value) + name: check.label ?? check.name, + })[matcher as AvailableMatchers](check.value) }) } diff --git a/app/javascript/components/bootcamp/types/Matchers.d.ts b/app/javascript/components/bootcamp/types/Matchers.d.ts index 4be6551611..3189156938 100644 --- a/app/javascript/components/bootcamp/types/Matchers.d.ts +++ b/app/javascript/components/bootcamp/types/Matchers.d.ts @@ -2,8 +2,8 @@ declare type AvailableMatchers = | 'toBe' | 'toBeTrue' | 'toBeFalse' - | 'toExist' - | 'toNotExist' + | 'toBeDefined' + | 'toBeUndefined' | 'toEqual' | 'toBeGreaterThanOrEqual' | 'toBeLessThanOrEqual' diff --git a/app/javascript/interpreter/environment.ts b/app/javascript/interpreter/environment.ts index b6f9eba215..87cd43bac3 100644 --- a/app/javascript/interpreter/environment.ts +++ b/app/javascript/interpreter/environment.ts @@ -7,8 +7,11 @@ import { isString } from './checks' export class Environment { private readonly values: Map = new Map() + public readonly id // Useful for debugging - constructor(private readonly enclosing: Environment | null = null) {} + constructor(private readonly enclosing: Environment | null = null) { + this.id = Math.random().toString(36).substring(7) + } public inScope(name: Token | string): boolean { const nameString = isString(name) ? name : name.lexeme @@ -28,7 +31,12 @@ export class Environment { public get(name: Token): any { if (this.values.has(name.lexeme)) return this.values.get(name.lexeme) - if (this.enclosing !== null) return this.enclosing.get(name) + + // Try the enclosing environment(s), but handle the error here so we can + // make use of the didYouMean function + try { + if (this.enclosing !== null) return this.enclosing.get(name) + } catch (e) {} const variableNames = Object.keys(this.variables()) const functionNames = Object.keys(this.functions()) @@ -48,23 +56,16 @@ export class Environment { ) } - public getAt(distance: number, name: string): any { - return this.ancestor(distance).values.get(name) - } - - private ancestor(distance: number) { - let environment: Environment = this - for (let i = 0; i < distance; i++) environment = environment.enclosing! - return environment - } - - public assign(name: Token, value: any): void { + public updateVariable(name: Token, value: any): void { if (this.values.has(name.lexeme)) { this.values.set(name.lexeme, value) return } - this.enclosing?.assign(name, value) + if (this.enclosing?.get(name)) { + this.enclosing?.updateVariable(name, value) + return + } throw new RuntimeError( translate('error.runtime.couldNotFindValueWithName', { @@ -75,10 +76,6 @@ export class Environment { ) } - public assignAt(distance: number, name: Token, value: any): void { - this.ancestor(distance).values.set(name.lexeme, value) - } - public variables(): Record { let current: Environment | null = this let vars: any = {} diff --git a/app/javascript/interpreter/error.ts b/app/javascript/interpreter/error.ts index 4c246318f7..09c41f78d6 100644 --- a/app/javascript/interpreter/error.ts +++ b/app/javascript/interpreter/error.ts @@ -4,6 +4,76 @@ export type DisabledLanguageFeatureErrorType = | 'ExcludeListViolation' | 'IncludeListViolation' +export type SyntaxErrorType = + | 'UnknownCharacter' + | 'MissingCommaAfterParameters' + | 'MissingDoToStartBlock' + | 'MissingEndAfterBlock' + | 'MissingConditionAfterIf' + | 'MissingDoubleQuoteToStartString' + | 'MissingDoubleQuoteToTerminateString' + | 'MissingFieldNameOrIndexAfterLeftBracket' + | 'MissingRightParenthesisAfterExpression' + | 'MissingRightBraceToTerminatePlaceholder' + | 'MissingBacktickToTerminateTemplateLiteral' + | 'MissingExpression' + | 'InvalidAssignmentTarget' + | 'ExceededMaximumNumberOfParameters' + | 'MissingEndOfLine' + | 'MissingFunctionName' + | 'MissingLeftParenthesisAfterFunctionName' + | 'MissingLeftParenthesisAfterFunctionCall' + | 'MissingParameterName' + | 'MissingRightParenthesisAfterParameters' + | 'MissingLeftParenthesisAfterWhile' + | 'MissingRightParenthesisAfterWhileCondition' + | 'MissingWhileBeforeDoWhileCondition' + | 'MissingLeftParenthesisAfterDoWhile' + | 'MissingRightParenthesisAfterDoWhileCondition' + | 'MissingVariableName' + | 'InvalidNumericVariableName' + | 'MissingConstantName' + | 'MissingToAfterVariableNameToInitializeValue' + | 'MissingToAfterVariableNameToChangeValue' + | 'MissingLeftParenthesisBeforeIfCondition' + | 'MissingRightParenthesisAfterIfCondition' + | 'MissingDoToStartFunctionBody' + | 'MissingDoToStartFunctionBody' + | 'MissingDoToStartIfBody' + | 'MissingDoToStartElseBody' + | 'MissingDoAfterRepeatStatementCondition' + | 'MissingDoAfterWhileStatementCondition' + | 'MissingLeftParenthesisAfterForeach' + | 'MissingLetInForeachCondition' + | 'MissingElementNameAfterForeach' + | 'MissingOfAfterElementNameInForeach' + | 'MissingRightParenthesisAfterForeachElement' + | 'MissingRightBracketAfterFieldNameOrIndex' + | 'MissingRightParenthesisAfterFunctionCall' + | 'MissingRightParenthesisAfterExpression' + | 'MissingRightParenthesisAfterExpressionWithPotentialTypo' + | 'MissingRightBracketAfterListElements' + | 'MissingRightBraceAfterMapElements' + | 'MissingWithBeforeParameters' + | 'MissingStringAsKey' + | 'MissingColonAfterKey' + | 'MissingFieldNameOrIndexAfterOpeningBracket' + | 'InvalidTemplateLiteral' + | 'MissingColonAfterThenBranchOfTernaryOperator' + | 'NumberEndsWithDecimalPoint' + | 'NumberWithMultipleDecimalPoints' + | 'NumberContainsAlpha' + | 'NumberStartsWithZero' + | 'UnexpectedElseWithoutIf' + | 'UnexpectedLiteralExpressionAfterIf' + | 'UnexpectedSpaceInIdentifier' + | 'UnexpectedVariableExpressionAfterIf' + | 'UnexpectedVariableExpressionAfterIfWithPotentialTypo' + | 'DuplicateParameterName' + | 'MissingTimesInRepeat' + | 'UnexpectedEqualsForAssignment' + | 'UnexpectedEqualsForEquality' + export type SemanticErrorType = | 'TopLevelReturn' | 'VariableUsedInOwnInitializer' @@ -37,6 +107,7 @@ export type RuntimeErrorType = | 'InvalidIndexGetterTarget' | 'InvalidIndexSetterTarget' | 'UnexpectedEqualsForEquality' + | 'VariableAlreadyDeclared' export type StaticErrorType = | DisabledLanguageFeatureErrorType diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index 16e2f91198..0c59b03d8c 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -1,9 +1,4 @@ -import { - type Callable, - ReturnValue, - UserDefinedFunction, - isCallable, -} from './functions' +import { ReturnValue, UserDefinedFunction, isCallable } from './functions' import { isArray, isBoolean, isNumber, isObject, isString } from './checks' import { Environment } from './environment' import { RuntimeError, type RuntimeErrorType, isRuntimeError } from './error' @@ -60,6 +55,7 @@ export type ExecutionContext = { getCurrentTime: Function fastForward: Function evaluate: Function + executeBlock: Function updateState: Function logicError: Function } @@ -88,7 +84,6 @@ export class Executor private readonly sourceCode: string, private languageFeatures: LanguageFeatures = {}, private externalFunctions: ExternalFunction[], - private locals: Map, private externalState: Record = {} ) { for (let externalFunction of externalFunctions) { @@ -167,10 +162,21 @@ export class Executor } } - public executeBlock(statements: Statement[], environment: Environment): void { + public executeBlock( + statements: Statement[], + blockEnvironment: Environment + ): void { + // Don't + if (this.environment === blockEnvironment) { + for (const statement of statements) { + this.executeStatement(statement) + } + return + } + const previous: Environment = this.environment try { - this.environment = environment + this.environment = blockEnvironment for (const statement of statements) { this.executeStatement(statement) @@ -211,16 +217,13 @@ export class Executor name: statement.name.lexeme, }) } - const result = this.evaluate(statement.initializer) - const updating = this.environment.inScope(statement.name.lexeme) - this.environment.define(statement.name.lexeme, result.value) + const value = this.evaluate(statement.initializer).value + this.environment.define(statement.name.lexeme, value) + return { type: 'VariableStatement', name: statement.name.lexeme, - value: result.value, - data: { - updating: updating, - }, + value: value, } }) } @@ -289,9 +292,7 @@ export class Executor count-- // Delay repeat for things like animations - if (this.languageFeatures?.repeatDelay) { - this.time += this.languageFeatures?.repeatDelay || 0 - } + this.time += this.languageFeatures.repeatDelay } } @@ -337,11 +338,17 @@ export class Executor } public visitBlockStatement(statement: BlockStatement): void { - this.executeBlock(statement.statements, new Environment(this.environment)) + // Change this to allow scoping + // this.executeBlock(statement.statements, new Environment(this.environment)) + this.executeBlock(statement.statements, this.environment) } public visitFunctionStatement(statement: FunctionStatement): void { - const func = new UserDefinedFunction(statement, this.environment) + const func = new UserDefinedFunction( + statement, + this.environment, + this.languageFeatures + ) this.environment.define(statement.name.lexeme, func) } @@ -384,6 +391,10 @@ export class Executor } }) + // 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) @@ -572,7 +583,7 @@ export class Executor switch (expression.operator.type) { case 'NOT': - this.verifyBooleanOperand(expression.operator, operand.value) + this.verifyBooleanOperand(operand.value, expression.operator.location) return { type: 'UnaryExpression', operator: expression.operator.type, @@ -724,13 +735,13 @@ export class Executor ): EvaluationResult { if (expression.operator.type === 'OR') { const leftOr = this.evaluate(expression.left) - this.verifyBooleanOperand(expression.operator, leftOr.value) + this.verifyBooleanOperand(leftOr.value, expression.operator.location) let rightOr: EvaluationResult | undefined = undefined if (!leftOr.value) { rightOr = this.evaluate(expression.right) - this.verifyBooleanOperand(expression.operator, rightOr.value) + this.verifyBooleanOperand(rightOr.value, expression.operator.location) } return { @@ -744,13 +755,13 @@ export class Executor } const leftAnd = this.evaluate(expression.left) - this.verifyBooleanOperand(expression.operator, leftAnd.value) + this.verifyBooleanOperand(leftAnd.value, expression.operator.location) let rightAnd: EvaluationResult | undefined = undefined if (leftAnd.value) { rightAnd = this.evaluate(expression.right) - this.verifyBooleanOperand(expression.operator, rightAnd.value) + this.verifyBooleanOperand(rightAnd.value, expression.operator.location) } return { @@ -800,7 +811,7 @@ export class Executor ? this.lookupVariable(expression.name, expression) / value.value : null - this.updateVariable(expression, expression.name, newValue) + this.updateVariable(expression.name, newValue, expression) return { type: 'AssignExpression', @@ -822,7 +833,7 @@ export class Executor newValue = expression.operator.type === 'PLUS_PLUS' ? value + 1 : value - 1 - this.updateVariable(expression.operand, expression.operand.name, newValue) + this.updateVariable(expression.operand.name, newValue, expression.operand) return { type: 'UpdateExpression', @@ -861,13 +872,14 @@ export class Executor } private updateVariable( - expression: Expression, name: Token, - newValue: undefined + newValue: undefined, + expression: Expression ) { - const distance = this.locals.get(expression) - if (distance === undefined) this.globals.assign(name, newValue) - else this.environment.assignAt(distance, name, newValue) + // This will exception if the variable doesn't exist + this.lookupVariable(name, expression) + + this.environment.updateVariable(name, newValue) } public visitGetExpression(expression: GetExpression): EvaluationResult { @@ -947,8 +959,7 @@ export class Executor private verifyBooleanOperand(operand: any, location: Location): void { if (isBoolean(operand)) return - if (this.languageFeatures?.truthiness === 'OFF') - this.error('OperandMustBeBoolean', location, { operand }) + this.error('OperandMustBeBoolean', location, { operand }) } public executeStatement(statement: Statement): void { @@ -959,10 +970,17 @@ export class Executor return expression.accept(this) } - private lookupVariable(name: Token, expression: VariableExpression): any { - const distance = this.locals.get(expression) - if (distance === undefined) return this.globals.get(name) - return this.environment.getAt(distance, name.lexeme) + private lookupVariable(name: Token, expression: Expression): any { + let value = this.environment.get(name) + if (value === undefined) { + this.globals.get(name) + } + if (value === undefined) { + this.error('CouldNotFindValueWithName', expression.location, { + name: name.lexeme, + }) + } + return value } private guardInfiniteLoop(loc: Location) { diff --git a/app/javascript/interpreter/functions.ts b/app/javascript/interpreter/functions.ts index f5bdece44f..b9c64d0bbc 100644 --- a/app/javascript/interpreter/functions.ts +++ b/app/javascript/interpreter/functions.ts @@ -1,7 +1,7 @@ import { Environment } from './environment' -import { Interpreter } from './interpreter' +import { LanguageFeatures } from './interpreter' import { FunctionStatement } from './statement' -import type { ExecutionContext } from './executor' +import type { ExecutionContext, Executor } from './executor' export type Arity = number | [min: number, max: number] @@ -23,7 +23,8 @@ export function isCallable(obj: any): obj is Callable { export class UserDefinedFunction implements Callable { constructor( private declaration: FunctionStatement, - private closure: Environment + private closure: Environment, + private languageFeatures: LanguageFeatures ) {} arity(): Arity { @@ -33,20 +34,25 @@ export class UserDefinedFunction implements Callable { ] } - call(interpreter: Interpreter, args: any[]): any { - const environment = new Environment(this.closure) + call(executor: ExecutionContext, args: any[]): any { + let environment + if (this.languageFeatures.allowGlobals) { + environment = new Environment(this.closure) + } else { + environment = new Environment() + } for (let i = 0; i < this.declaration.parameters.length; i++) { const arg = i < args.length ? args[i] - : interpreter.evaluate(this.declaration.parameters[i].defaultValue!) + : executor.evaluate(this.declaration.parameters[i].defaultValue!) .value environment.define(this.declaration.parameters[i].name.lexeme, arg) } try { - interpreter.executeBlock(this.declaration.body, environment) + executor.executeBlock(this.declaration.body, environment) } catch (error: unknown) { if (error instanceof ReturnValue) { return error.value diff --git a/app/javascript/interpreter/languages/jikiscript/helpers/complexErrors.ts b/app/javascript/interpreter/helpers/complexErrors.ts similarity index 100% rename from app/javascript/interpreter/languages/jikiscript/helpers/complexErrors.ts rename to app/javascript/interpreter/helpers/complexErrors.ts diff --git a/app/javascript/interpreter/languages/jikiscript/helpers/isTypo.ts b/app/javascript/interpreter/helpers/isTypo.ts similarity index 100% rename from app/javascript/interpreter/languages/jikiscript/helpers/isTypo.ts rename to app/javascript/interpreter/helpers/isTypo.ts diff --git a/app/javascript/interpreter/interpreter.ts b/app/javascript/interpreter/interpreter.ts index 21694cbca7..05a3da4a11 100644 --- a/app/javascript/interpreter/interpreter.ts +++ b/app/javascript/interpreter/interpreter.ts @@ -1,43 +1,14 @@ -import { - RuntimeError, - type RuntimeErrorType, - type StaticError, - isStaticError, -} from './error' +import { RuntimeError, type RuntimeErrorType, type StaticError } from './error' import { Expression } from './expression' import { Location } from './location' -import { Parser as JavaScriptParser } from './languages/javascript/parser' -import { Parser as JikiScriptParser } from './languages/jikiscript/parser' +import { Parser } from './parser' import { Executor } from './executor' import { Statement } from './statement' import type { TokenType } from './token' -import { Resolver } from './resolver' import { translate } from './translator' import type { ExternalFunction } from './executor' import type { Frame } from './frames' -export type Language = 'JikiScript' | 'JavaScript' -const LanguageSettings = { - JikiScript: { - allowVariableReassigmment: true, - }, - JavaScript: { - allowVariableReassigmment: false, - }, -} - -interface ParserConstructor { - new ( - functionNames: string[], - languageFeatures: any, - wrapTopLevelStatements: boolean - ): Parser -} - -export interface Parser { - parse(sourceCode: string): Statement[] -} - export type FrameContext = { result: any expression?: Expression @@ -49,15 +20,20 @@ export type Toggle = 'ON' | 'OFF' export type LanguageFeatures = { includeList?: TokenType[] excludeList?: TokenType[] - shadowing?: Toggle - truthiness?: Toggle + repeatDelay: number + allowGlobals: boolean +} + +export type InputLanguageFeatures = { + includeList?: TokenType[] + excludeList?: TokenType[] repeatDelay?: number + allowGlobals?: boolean } export type Context = { externalFunctions?: ExternalFunction[] - language?: Language - languageFeatures?: LanguageFeatures + languageFeatures?: InputLanguageFeatures state?: Record wrapTopLevelStatements?: boolean } @@ -73,12 +49,6 @@ export type InterpretResult = { error: StaticError | null } -export function interpretJavaScript(sourceCode: string, context: Context = {}) { - return interpret(sourceCode, { ...context, language: 'JavaScript' }) -} -export function interpretJikiScript(sourceCode: string, context: Context = {}) { - return interpret(sourceCode, { ...context, language: 'JikiScript' }) -} export function compile(sourceCode: string, context: Context = {}) { const interpreter = new Interpreter(sourceCode, context) try { @@ -101,32 +71,6 @@ export function interpret( return interpreter.execute() } -export function evaluateJavaScriptFunction( - sourceCode: string, - context: Context = {}, - functionCall: string, - ...args: any[] -): EvaluateFunctionResult { - return evaluateFunction( - sourceCode, - { ...context, language: 'JavaScript' }, - functionCall, - ...args - ) -} -export function evaluateJikiScriptFunction( - sourceCode: string, - context: Context = {}, - functionCall: string, - ...args: any[] -): EvaluateFunctionResult { - return evaluateFunction( - sourceCode, - { ...context, language: 'JikiScript' }, - functionCall, - ...args - ) -} export function evaluateFunction( sourceCode: string, context: Context = {}, @@ -140,12 +84,9 @@ export function evaluateFunction( export class Interpreter { private readonly parser: Parser - private readonly resolver: Resolver private state: Record = {} - private language: Language - private parserType: ParserConstructor - private languageFeatures: LanguageFeatures = {} + private languageFeatures: LanguageFeatures private externalFunctions: ExternalFunction[] = [] private wrapTopLevelStatements = false @@ -153,35 +94,31 @@ export class Interpreter { constructor(private readonly sourceCode: string, context: Context) { // Set the instance variables based on the context that's been passed in. - this.language = context.language ? context.language : 'JavaScript' - this.parserType = - this.language == 'JavaScript' ? JavaScriptParser : JikiScriptParser - if (context.state !== undefined) { this.state = context.state } this.externalFunctions = context.externalFunctions ? context.externalFunctions : [] - if (context.languageFeatures !== undefined) { - this.languageFeatures = context.languageFeatures + + this.languageFeatures = { + includeList: undefined, + excludeList: undefined, + repeatDelay: 0, + allowGlobals: false, + ...context.languageFeatures, } - this.parser = new this.parserType( + this.parser = new Parser( this.externalFunctions.map((f) => f.name), this.languageFeatures, this.wrapTopLevelStatements ) - this.resolver = new Resolver( - LanguageSettings[this.language].allowVariableReassigmment, - this.externalFunctions.map((f) => f.name) - ) } public compile() { try { this.statements = this.parser.parse(this.sourceCode) - this.resolver.resolve(this.statements) } catch (error: unknown) { throw { frames: [], error: error } } @@ -192,7 +129,6 @@ export class Interpreter { this.sourceCode, this.languageFeatures, this.externalFunctions, - this.resolver.locals, this.state ) return executor.execute(this.statements) @@ -208,7 +144,7 @@ export class Interpreter { // Create a new parser with wrapTopLevelStatements set to false // and use it to generate the calling statements. - const callingStatements = new this.parserType( + const callingStatements = new Parser( this.externalFunctions.map((f) => f.name), this.languageFeatures, false @@ -219,28 +155,15 @@ export class Interpreter { callingStatements, }) - try { - this.resolver.resolve(callingStatements) - } catch (error: unknown) { - if (isStaticError(error)) { - return { value: undefined, frames: [], error: error } - } - } - const executor = new Executor( this.sourceCode, this.languageFeatures, - this.externalFunctions, - this.resolver.locals + this.externalFunctions ) executor.execute(this.statements) return executor.evaluateSingleExpression(callingStatements[0]) } - // public resolve(expression: Expression, depth: number): void { - // this.resolver.locals.set(expression, depth); - // } - private error( type: RuntimeErrorType, location: Location | null, diff --git a/app/javascript/interpreter/languages/javascript/error.ts b/app/javascript/interpreter/languages/javascript/error.ts deleted file mode 100644 index 9e6bee0b10..0000000000 --- a/app/javascript/interpreter/languages/javascript/error.ts +++ /dev/null @@ -1,56 +0,0 @@ -export type SyntaxErrorType = - | 'UnknownCharacter' - | 'MissingDoubleQuoteToStartString' - | 'MissingDoubleQuoteToTerminateString' - | 'MissingFieldNameOrIndexAfterLeftBracket' - | 'MissingRightParenthesisAfterExpression' - | 'MissingRightBraceToTerminatePlaceholder' - | 'MissingBacktickToTerminateTemplateLiteral' - | 'MissingExpression' - | 'InvalidAssignmentTarget' - | 'ExceededMaximumNumberOfParameters' - | 'MissingEndOfLine' - | 'MissingFunctionName' - | 'MissingLeftParenthesisAfterFunctionName' - | 'MissingLeftParenthesisAfterFunctionCall' - | 'MissingParameterName' - | 'MissingRightParenthesisAfterParameters' - | 'MissingLeftBraceToStartFunctionBody' - | 'MissingLeftBraceToStartWhileBody' - | 'MissingLeftParenthesisAfterWhile' - | 'MissingRightParenthesisAfterWhileCondition' - | 'MissingLeftBraceToStartDoWhileBody' - | 'MissingWhileBeforeDoWhileCondition' - | 'MissingLeftParenthesisAfterDoWhile' - | 'MissingRightParenthesisAfterDoWhileCondition' - | 'MissingLeftBraceToStartRepeatBody' - | 'MissingVariableName' - | 'MissingConstantName' - | 'MissingEqualsSignAfterVariableNameToInitializeValue' - | 'MissingEqualsSignAfterConstantNameToInitializeValue' - | 'MissingLeftParenthesisBeforeIfCondition' - | 'MissingRightParenthesisAfterIfCondition' - | 'MissingLeftBraceToStartFunctionBody' - | 'MissingLeftBraceToStartFunctionBody' - | 'MissingLeftBraceToStartIfBody' - | 'MissingLeftBraceToStartElseBody' - | 'MissingDoAfterRepeatStatementCondition' - | 'MissingDoAfterWhileStatementCondition' - | 'MissingLeftParenthesisAfterForeach' - | 'MissingLetInForeachCondition' - | 'MissingElementNameAfterForeach' - | 'MissingOfAfterElementNameInForeach' - | 'MissingRightParenthesisAfterForeachElement' - | 'MissingLeftBraceToStartForeachBody' - | 'MissingRightBraceAfterBlock' - | 'MissingRightBracketAfterFieldNameOrIndex' - | 'MissingRightParenthesisAfterFunctionCall' - | 'MissingRightParenthesisAfterExpression' - | 'MissingRightBracketAfterListElements' - | 'MissingRightBraceAfterMapElements' - | 'MissingStringAsKey' - | 'MissingColonAfterKey' - | 'MissingFieldNameOrIndexAfterOpeningBracket' - | 'InvalidTemplateLiteral' - | 'MissingColonAfterThenBranchOfTernaryOperator' - | 'NumberWithMultipleDecimalPoints' diff --git a/app/javascript/interpreter/languages/javascript/parser.ts b/app/javascript/interpreter/languages/javascript/parser.ts deleted file mode 100644 index 5f0483b0eb..0000000000 --- a/app/javascript/interpreter/languages/javascript/parser.ts +++ /dev/null @@ -1,864 +0,0 @@ -import { SyntaxError } from '../../error' -import { type SyntaxErrorType } from './error' -import { - ArrayExpression, - AssignExpression, - BinaryExpression, - CallExpression, - Expression, - GroupingExpression, - LiteralExpression, - LogicalExpression, - DictionaryExpression, - UnaryExpression, - VariableExpression, - GetExpression, - SetExpression, - TemplateLiteralExpression, - TemplatePlaceholderExpression, - TemplateTextExpression, - UpdateExpression, - TernaryExpression, -} from '../../expression' -import type { LanguageFeatures } from '../../interpreter' -import { Location } from '../../location' -import { Scanner } from './scanner' -import { - BlockStatement, - ConstantStatement, - DoWhileStatement, - ExpressionStatement, - ForeachStatement, - FunctionParameter, - FunctionStatement, - IfStatement, - RepeatStatement, - RepeatUntilGameOverStatement, - ReturnStatement, - Statement, - VariableStatement, - WhileStatement, -} from '../../statement' -import type { Token, TokenType } from './token' -import { translate } from '../../translator' - -export class Parser { - private readonly scanner: Scanner - private current: number = 0 - private tokens: Token[] = [] - - constructor( - private functionNames: string[] = [], - languageFeatures: LanguageFeatures, - private shouldWrapTopLevelStatements: boolean - ) { - this.scanner = new Scanner(languageFeatures) - } - - public parse(sourceCode: string): Statement[] { - this.tokens = this.scanner.scanTokens(sourceCode) - - const statements = [] - - while (!this.isAtEnd()) statements.push(this.declarationStatement()) - - if (this.shouldWrapTopLevelStatements) - return this.wrapTopLevelStatements(statements) - - return statements - } - - wrapTopLevelStatements(statements: Statement[]): Statement[] { - const functionStmt = new FunctionStatement( - { - type: 'IDENTIFIER', - lexeme: 'main', - literal: null, - location: Location.unknown, - }, - [], - [], - Location.unknown - ) - - for (let i = statements.length - 1; i >= 0; i--) { - // Don't wrap top-level function statements - if (statements[i] instanceof FunctionStatement) continue - - functionStmt.body.unshift(statements[i]) - statements.splice(i, 1) - } - - statements.push(functionStmt) - return statements - } - - private declarationStatement(): Statement { - if (this.match('FUNCTION')) return this.functionStatement() - - return this.statement() - } - - private functionStatement(): Statement { - const name = this.consume('IDENTIFIER', 'MissingFunctionName') - this.consume('LEFT_PAREN', 'MissingLeftParenthesisAfterFunctionName', { - name, - }) - const parameters: FunctionParameter[] = [] - if (!this.check('RIGHT_PAREN')) { - do { - if (parameters.length > 255) { - this.error( - 'ExceededMaximumNumberOfParameters', - this.peek().location, - { - maximum: 255, - actual: parameters.length, - name, - } - ) - } - - const parameterName = this.consume( - 'IDENTIFIER', - 'MissingParameterName', - { - name: name, - } - ) - - let defaultValue: Expression | null = null - - if (this.match('EQUAL')) { - defaultValue = this.expression() - } - - parameters.push(new FunctionParameter(parameterName, defaultValue)) - } while (this.match('COMMA')) - } - this.consume('RIGHT_PAREN', 'MissingRightParenthesisAfterParameters', { - name, - parameters, - }) - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartFunctionBody', { name }) - this.consumeEndOfLine() - - const body = this.block() - this.functionNames.push(name.lexeme) - return new FunctionStatement( - name, - parameters, - body, - Location.between(name, this.previous()) - ) - } - - private statement(): Statement { - if (this.match('LET')) return this.letStatement() - if (this.match('CONST')) return this.constStatement() - if (this.match('IF')) return this.ifStatement() - if (this.match('RETURN')) return this.returnStatement() - if (this.match('REPEAT_UNTIL_GAME_OVER')) - return this.repeatUntilGameOverStatement() - if (this.match('WHILE')) return this.whileStatement() - if (this.match('DO')) return this.doWhileStatement() - if (this.match('FOR')) return this.foreachStatement() - if (this.match('LEFT_BRACE')) return this.blockStatement() - - return this.expressionStatement() - } - - private letStatement(): Statement { - const letToken = this.previous() - const name = this.consume('IDENTIFIER', 'MissingVariableName') - this.consume( - 'EQUAL', - 'MissingEqualsSignAfterVariableNameToInitializeValue', - { - name, - } - ) - - const initializer = this.expression() - this.consumeEndOfLine() - - return new VariableStatement( - name, - initializer, - Location.between(letToken, initializer) - ) - } - - private constStatement(): Statement { - const constToken = this.previous() - const name = this.consume('IDENTIFIER', 'MissingConstantName') - this.consume( - 'EQUAL', - 'MissingEqualsSignAfterConstantNameToInitializeValue', - { - name, - } - ) - - const initializer = this.expression() - this.consumeEndOfLine() - - return new ConstantStatement( - name, - initializer, - Location.between(constToken, initializer) - ) - } - - private ifStatement(): Statement { - const ifToken = this.previous() - this.consume('LEFT_PAREN', 'MissingLeftParenthesisBeforeIfCondition') - const condition = this.expression() - this.consume('RIGHT_PAREN', 'MissingRightParenthesisAfterIfCondition', { - condition, - }) - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartIfBody') - const thenBranch = this.blockStatement() - let elseBranch = null - - if (this.match('ELSE')) { - if (this.match('IF')) { - elseBranch = this.ifStatement() - } else { - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartElseBody') - elseBranch = this.blockStatement() - } - } - - return new IfStatement( - condition, - thenBranch, - elseBranch, - Location.between(ifToken, this.previous()) - ) - } - - private returnStatement(): Statement { - const keyword = this.previous() - const value: Expression | null = this.isAtEndOfStatement() - ? null - : this.expression() - - this.consumeEndOfLine() - - return new ReturnStatement( - keyword, - value, - Location.between(keyword, value || keyword) - ) - } - - private repeatUntilGameOverStatement(): Statement { - const begin = this.previous() - - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartRepeatBody') - this.consumeEndOfLine() - - const statements = this.block() - - return new RepeatUntilGameOverStatement( - statements, - Location.between(begin, this.previous()) - ) - } - - private whileStatement(): Statement { - const begin = this.previous() - this.consume('LEFT_PAREN', 'MissingLeftParenthesisAfterWhile') - const condition = this.expression() - this.consume('RIGHT_PAREN', 'MissingRightParenthesisAfterWhileCondition', { - condition, - }) - - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartWhileBody') - this.consumeEndOfLine() - - const statements = this.block() - - return new WhileStatement( - condition, - statements, - Location.between(begin, this.previous()) - ) - } - - private doWhileStatement(): Statement { - const begin = this.previous() - - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartDoWhileBody') - this.consumeEndOfLine() - - const statements = this.block() - - this.consume('WHILE', 'MissingWhileBeforeDoWhileCondition') - - this.consume('LEFT_PAREN', 'MissingLeftParenthesisAfterDoWhile') - const condition = this.expression() - this.consume( - 'RIGHT_PAREN', - 'MissingRightParenthesisAfterDoWhileCondition', - { - condition, - } - ) - - this.consumeEndOfLine() - - return new DoWhileStatement( - condition, - statements, - Location.between(begin, this.previous()) - ) - } - - private foreachStatement(): Statement { - const foreachToken = this.previous() - this.consume('LEFT_PAREN', 'MissingLeftParenthesisAfterForeach') - this.consume('LET', 'MissingLetInForeachCondition') - const elementName = this.consume( - 'IDENTIFIER', - 'MissingElementNameAfterForeach' - ) - this.consume('OF', 'MissingOfAfterElementNameInForeach', { - elementName, - }) - const iterable = this.expression() - - this.consume('RIGHT_PAREN', 'MissingRightParenthesisAfterForeachElement', { - iterable, - }) - this.consume('LEFT_BRACE', 'MissingLeftBraceToStartForeachBody') - this.consumeEndOfLine() - - const statements = this.block() - - return new ForeachStatement( - elementName, - iterable, - statements, - Location.between(foreachToken, this.previous()) - ) - } - - private blockStatement(): BlockStatement { - const leftBraceToken = this.previous() - this.consumeEndOfLine() - const statements = this.block() - - return new BlockStatement( - statements, - Location.between(leftBraceToken, this.previous()) - ) - } - - private block(): Statement[] { - const statements: Statement[] = [] - - while (!this.check('RIGHT_BRACE') && !this.isAtEnd()) { - statements.push(this.statement()) - } - - this.consume('RIGHT_BRACE', 'MissingRightBraceAfterBlock') - this.consumeEndOfLine() - return statements - } - - private expressionStatement(): Statement { - const expression = this.expression() - this.consumeEndOfLine() - - return new ExpressionStatement(expression, expression.location) - } - - private expression(): Expression { - return this.assignment() - } - - private assignment(): Expression { - const expr = this.ternary() - - if ( - this.match( - 'EQUAL', - 'SLASH_EQUAL', - 'STAR_EQUAL', - 'PLUS_EQUAL', - 'MINUS_EQUAL' - ) - ) { - const operator = this.previous() - const value = this.assignment() - - if (expr instanceof VariableExpression) { - return new AssignExpression( - expr.name, - operator, - value, - Location.between(expr, value) - ) - } - - if (expr instanceof GetExpression) { - return new SetExpression( - expr.obj, - expr.field, - value, - Location.between(expr, value) - ) - } - - this.error('InvalidAssignmentTarget', expr.location, { - assignmentTarget: expr, - }) - } - - return expr - } - - private ternary(): Expression { - const expr = this.or() - - if (this.match('QUESTION_MARK')) { - const then = this.ternary() - this.consume('COLON', 'MissingColonAfterThenBranchOfTernaryOperator', { - then, - }) - const else_ = this.ternary() - return new TernaryExpression( - expr, - then, - else_, - Location.between(expr, else_) - ) - } - - return expr - } - - private or(): Expression { - const expr = this.and() - - while (this.match('OR', 'PIPE_PIPE')) { - let operator = this.previous() - operator.type = 'OR' - const right = this.and() - return new LogicalExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private and(): Expression { - const expr = this.equality() - - while (this.match('AND', 'AMPERSAND_AMPERSAND')) { - let operator = this.previous() - operator.type = 'AND' - const right = this.equality() - return new LogicalExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private equality(): Expression { - let expr = this.comparison() - - while ( - this.match( - 'EQUALITY', - 'STRICT_INEQUALITY', - 'INEQUALITY', - 'STRICT_EQUALITY' - ) - ) { - const operator = this.previous() - const right = this.comparison() - expr = new BinaryExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private comparison(): Expression { - let expr = this.term() - - while (this.match('GREATER', 'GREATER_EQUAL', 'LESS', 'LESS_EQUAL')) { - const operator = this.previous() - const right = this.term() - expr = new BinaryExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private term(): Expression { - let expr = this.factor() - - while (this.match('MINUS', 'PLUS')) { - const operator = this.previous() - const right = this.factor() - expr = new BinaryExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private factor(): Expression { - let expr = this.unary() - - while (this.match('SLASH', 'STAR')) { - const operator = this.previous() - const right = this.unary() - expr = new BinaryExpression( - expr, - operator, - right, - Location.between(expr, right) - ) - } - - return expr - } - - private unary(): Expression { - if (this.match('NOT', 'MINUS')) { - const operator = this.previous() - const right = this.unary() - return new UnaryExpression( - operator, - right, - Location.between(operator, right) - ) - } - - return this.postfix() - } - - private postfix(): Expression { - const expression = this.call() - - if (this.match('PLUS_PLUS', 'MINUS_MINUS')) { - const operator = this.previous() - - return new UpdateExpression( - expression, - operator, - Location.between(operator, expression) - ) - } - - return expression - } - - private call(): Expression { - let expression = this.primary() - - while (true) { - if (this.match('LEFT_PAREN')) { - expression = this.finishCall(expression) - } else if (this.match('LEFT_BRACKET')) { - const leftBracket = this.previous() - if (!this.match('STRING', 'NUMBER')) - this.error( - 'MissingFieldNameOrIndexAfterLeftBracket', - leftBracket.location, - { - expression, - } - ) - - const name = this.previous() - const rightBracket = this.consume( - 'RIGHT_BRACKET', - 'MissingRightBracketAfterFieldNameOrIndex', - { expression, name } - ) - expression = new GetExpression( - expression, - name, - Location.between(expression, rightBracket) - ) - } else { - if ( - expression instanceof VariableExpression && - this.functionNames.includes(expression.name.lexeme) && - this.match('RIGHT_PAREN') - ) - this.error( - 'MissingLeftParenthesisAfterFunctionCall', - this.previous().location, - { expression, function: expression.name.lexeme } - ) - break - } - } - - return expression - } - - private finishCall(callee: Expression): Expression { - const args: Expression[] = [] - - if (!this.check('RIGHT_PAREN')) { - do { - args.push(this.expression()) - } while (this.match('COMMA')) - } - - const paren = this.consume( - 'RIGHT_PAREN', - 'MissingRightParenthesisAfterFunctionCall', - { - args, - function: - callee instanceof VariableExpression ? callee.name.lexeme : null, - } - ) - return new CallExpression( - callee, - paren, - args, - Location.between(callee, paren) - ) - } - - private primary(): Expression { - if (this.match('LEFT_BRACKET')) return this.array() - - if (this.match('LEFT_BRACE')) return this.dictionary() - - if (this.match('FALSE')) - return new LiteralExpression(false, this.previous().location) - - if (this.match('TRUE')) - return new LiteralExpression(true, this.previous().location) - - if (this.match('NULL')) - return new LiteralExpression(null, this.previous().location) - - if (this.match('NUMBER', 'STRING')) - return new LiteralExpression( - this.previous().literal, - this.previous().location - ) - - if (this.match('IDENTIFIER')) - return new VariableExpression(this.previous(), this.previous().location) - - if (this.match('BACKTICK')) return this.templateLiteral() - - if (this.match('LEFT_PAREN')) { - const lparen = this.previous() - const expression = this.expression() - const rparen = this.consume( - 'RIGHT_PAREN', - 'MissingRightParenthesisAfterExpression', - { - expression, - } - ) - return new GroupingExpression( - expression, - Location.between(lparen, rparen) - ) - } - - this.error('MissingExpression', this.peek().location) - } - - private templateLiteral(): Expression { - const openBacktick = this.previous() - const parts: Expression[] = [] - - while (this.peek().type != 'BACKTICK') { - if (this.match('DOLLAR_LEFT_BRACE')) { - const dollarLeftBrace = this.previous() - const expr = this.expression() - const rightBrace = this.consume( - 'RIGHT_BRACE', - 'MissingRightBraceToTerminatePlaceholder', - { expr } - ) - parts.push( - new TemplatePlaceholderExpression( - expr, - Location.between(dollarLeftBrace, rightBrace) - ) - ) - } else { - const textToken = this.consume( - 'TEMPLATE_LITERAL_TEXT', - 'InvalidTemplateLiteral' - ) - parts.push(new TemplateTextExpression(textToken, textToken.location)) - } - } - - const closeBacktick = this.consume( - 'BACKTICK', - 'MissingBacktickToTerminateTemplateLiteral', - { elements: parts } - ) - return new TemplateLiteralExpression( - parts, - Location.between(openBacktick, closeBacktick) - ) - } - - private array(): Expression { - const leftBracket = this.previous() - const elements: Expression[] = [] - - if (!this.check('RIGHT_BRACKET')) { - do { - elements.push(this.or()) - } while (this.match('COMMA')) - } - - const rightBracket = this.consume( - 'RIGHT_BRACKET', - 'MissingRightBracketAfterListElements', - { elements } - ) - return new ArrayExpression( - elements, - Location.between(leftBracket, rightBracket) - ) - } - - private dictionary(): Expression { - const leftBrace = this.previous() - const elements = new Map() - - if (!this.check('RIGHT_BRACE')) { - do { - const key = this.consume('STRING', 'MissingStringAsKey') - this.consume('COLON', 'MissingColonAfterKey') - elements.set(key.literal, this.primary()) - } while (this.match('COMMA')) - } - - const rightBracket = this.consume( - 'RIGHT_BRACE', - 'MissingRightBraceAfterMapElements', - { elements } - ) - return new DictionaryExpression( - elements, - Location.between(leftBrace, rightBracket) - ) - } - - private match(...tokenTypes: TokenType[]): boolean { - for (const tokenType of tokenTypes) { - if (this.check(tokenType)) { - this.advance() - return true - } - } - return false - } - - private check(tokenType: TokenType): boolean { - if (this.isAtEnd()) return false - return this.peek().type == tokenType - } - - private advance(): Token { - if (!this.isAtEnd()) this.current++ - return this.previous() - } - - private consume( - tokenType: TokenType, - type: SyntaxErrorType, - context?: any - ): Token { - if (this.check(tokenType)) return this.advance() - - this.error(type, this.peek().location, context) - } - - private consumeEndOfLine(): void { - this.consume('EOL', 'MissingEndOfLine') - } - - private error( - type: SyntaxErrorType, - location: Location, - context?: any - ): never { - throw new SyntaxError( - translate(`error.syntax.${type}`, context), - location, - type, - context - ) - } - - private isAtEnd(): boolean { - return this.peek().type == 'EOF' - } - - private isAtEndOfStatement(): boolean { - return this.peek().type == 'EOL' || this.isAtEnd() - } - - private peek(): Token { - return this.tokens[this.current] - } - - private previous(): Token { - return this.tokens[this.current - 1] - } -} - -export function parse( - sourceCode: string, - { - functionNames = [], - languageFeatures = {}, - shouldWrapTopLevelStatements = false, - }: { - functionNames?: string[] - languageFeatures?: LanguageFeatures - shouldWrapTopLevelStatements?: boolean - } = {} -): Statement[] { - return new Parser( - functionNames, - languageFeatures, - shouldWrapTopLevelStatements - ).parse(sourceCode) -} diff --git a/app/javascript/interpreter/languages/javascript/scanner.ts b/app/javascript/interpreter/languages/javascript/scanner.ts deleted file mode 100644 index 79d94099c3..0000000000 --- a/app/javascript/interpreter/languages/javascript/scanner.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* - * The scanner is the first part of the interpreter. - * It takes the source code as input and produces a list of tokens, that - * represent the different conceptual elements of the source code. For example, - * it takes a whole string and reduces it into a STRING token, and it takes an - * equals sign and turns it into an EQUAL token. - * - * These tokens will then be used by the parser to build a tree of expressions, - * which will be used by the interpreter to execute the program. - * - * The main workflow here is looking at the next character in the source code, - * and then deciding what to do with it. Sometimes we just immediate "consume" it - * (turn it into a token), but other times we need to peak ahead and see what comes - * next to know what token to produce or how to handle it. - * - * This process will also produce errors if the source code is invalid, for example - * if we see an unterminated string, or a number with multiple decimal points. - */ -import { - DisabledLanguageFeatureError, - type DisabledLanguageFeatureErrorType, - SyntaxError, -} from '../../error' -import { type SyntaxErrorType } from './error' -import type { Token, TokenType } from './token' -import { Location } from '../../location' -import type { LanguageFeatures } from '../../interpreter' -import { translate } from '../../translator' - -export class Scanner { - private tokens: Token[] = [] - private start: number = 0 - private current: number = 0 - private line: number = 1 - private lineOffset: number = 0 - private sourceCode: string = '' - - private static readonly keywords: Record = { - and: 'AND', - const: 'CONST', - do: 'DO', - else: 'ELSE', - false: 'FALSE', - for: 'FOR', - function: 'FUNCTION', - if: 'IF', - in: 'IN', - let: 'LET', - null: 'NULL', - of: 'OF', - or: 'OR', - repeatUntilGameOver: 'REPEAT_UNTIL_GAME_OVER', - return: 'RETURN', - true: 'TRUE', - while: 'WHILE', - } - - private readonly tokenizers: Record = { - '(': this.tokenizeLeftParanthesis, - ')': this.tokenizeRightParanthesis, - '{': this.tokenizeLeftBrace, - '}': this.tokenizeRightBrace, - '[': this.tokenizeLeftBracket, - ']': this.tokenizeRightBracket, - ':': this.tokenizeColon, - ',': this.tokenizeComma, - '+': this.tokenizePlus, - '-': this.tokenizeMinus, - '*': this.tokenizeStar, - '/': this.tokenizeSlash, - '=': this.tokenizeEquals, - '!': this.tokenizeBang, - '>': this.tokenizeGreater, - '<': this.tokenizeLess, - '?': this.tokenizeQuestionMark, - ' ': this.tokenizeWhitespace, - '\t': this.tokenizeWhitespace, - '\r': this.tokenizeWhitespace, - '\n': this.tokenizeNewline, - '"': this.tokenizeString, - '`': this.tokenizeTemplateLiteral, - } - - constructor(private languageFeatures: LanguageFeatures = {}) {} - - scanTokens(sourceCode: string): Token[] { - this.sourceCode = sourceCode - this.reset() - - while (!this.isAtEnd()) { - this.start = this.current - this.scanToken() - } - - // Add synthetic EOL token to simplify parsing - if (this.shouldAddEOLToken()) this.addSyntheticToken('EOL', '\n') - - // Add synthetic EOF token to simplify parsing - this.addSyntheticToken('EOF', '\0') - - return this.tokens - } - - private scanToken(): void { - const c = this.advance() - - const tokenizer = this.tokenizers[c] - if (tokenizer) { - tokenizer.bind(this)() - } else { - if (c == '&' && this.match('&')) { - this.addToken('AMPERSAND_AMPERSAND') - } else if (c == '|' && this.match('|')) { - this.addToken('PIPE_PIPE') - } else if (this.isDigit(c)) { - this.tokenizeNumber() - } else if (this.isAlpha(c)) { - this.tokenizeIdentifier() - } else { - this.error('UnknownCharacter', { - character: c, - }) - } - } - } - - /** - * These are tokenizers. The purpose of a tokenizer is to consume characters from the source code - * and produce a token. The token is then added to the list of tokens. - * - * For example, if we see a left paranthesis, we add a token of type "LEFT_PAREN" to the list of tokens. - * - * Some tokens are more complex. For example if we see an equals sign, we need to check if the next character - * is also an equals sign. If it is, we add a token of type "EQUAL_EQUAL" to the list of tokens. If it is not, - * we add a token of type "EQUAL" to the list of tokens. - * - * Some are even more complex. For example, if we see a double quote, we need to consume characters until we see - * another double quote. We then add a token of type "STRING" to the list of tokens - * with all the characters between the double quotes consumed. - */ - - /* This first set of tokenizers are simple. They consume a single character and add a token to the list of tokens, - * or do simple checks for the next characters (e.g. "++") - */ - private tokenizeLeftParanthesis() { - this.addToken('LEFT_PAREN') - } - private tokenizeRightParanthesis() { - this.addToken('RIGHT_PAREN') - } - private tokenizeLeftBrace() { - this.addToken('LEFT_BRACE') - } - private tokenizeRightBrace() { - this.addToken('RIGHT_BRACE') - } - private tokenizeLeftBracket() { - this.addToken('LEFT_BRACKET') - } - private tokenizeRightBracket() { - this.addToken('RIGHT_BRACKET') - } - private tokenizeColon() { - this.addToken('COLON') - } - private tokenizeComma() { - this.addToken('COMMA') - } - private tokenizePlus() { - this.addToken( - this.match('=') ? 'PLUS_EQUAL' : this.match('+') ? 'PLUS_PLUS' : 'PLUS' - ) - } - private tokenizeMinus() { - this.addToken( - this.match('=') - ? 'MINUS_EQUAL' - : this.match('-') - ? 'MINUS_MINUS' - : 'MINUS' - ) - } - private tokenizeStar() { - this.addToken(this.match('=') ? 'STAR_EQUAL' : 'STAR') - } - private tokenizeSlash() { - if (this.match('=')) { - this.addToken('SLASH_EQUAL') - } else { - this.addToken('SLASH') - } - } - private tokenizeEquals() { - this.addToken( - this.match('=') - ? this.match('=') - ? 'STRICT_EQUALITY' - : 'EQUALITY' - : 'EQUAL' - ) - } - private tokenizeBang() { - this.addToken( - this.match('=') - ? this.match('=') - ? 'STRICT_INEQUALITY' - : 'INEQUALITY' - : 'NOT' - ) - } - private tokenizeGreater() { - this.addToken(this.match('=') ? 'GREATER_EQUAL' : 'GREATER') - } - private tokenizeLess() { - this.addToken(this.match('=') ? 'LESS_EQUAL' : 'LESS') - } - private tokenizeQuestionMark() { - this.addToken('QUESTION_MARK') - } - - /* - * We don't tokenize whitespace, but we do need to match on it - */ - private tokenizeWhitespace() { - return - } - - /* - * The new line tokenizer not only adds a token, but also increments the line number - * and resets the line offset to the next character. - */ - private tokenizeNewline() { - if (this.shouldAddEOLToken()) this.addToken('EOL') - - this.line++ - this.lineOffset = this.current - } - - private tokenizeString(): void { - // Keep consuming characters until we see another double quote - // and then stop before we consume it. - while (this.peek() != '"' && this.isAnotherCharacter()) this.advance() - - // If we reach the end of the line, we have an unterminated string - if (this.peek() != '"') - if (this.previouslyAddedToken() == 'IDENTIFIER') - this.error('MissingDoubleQuoteToStartString', { - string: this.tokens[this.tokens.length - 1].lexeme, - }) - else - this.error('MissingDoubleQuoteToTerminateString', { - string: this.sourceCode.substring(this.start + 1, this.current), - }) - - // Consume the closing quotation mark - this.advance() - - // Finally add the token, with its value set to the characters between the quotes - this.addToken( - 'STRING', - this.sourceCode.substring(this.start + 1, this.current - 1) - ) - } - - // TODO: Check whether this errors correctly if split over lines - private tokenizeTemplateLiteral(): void { - this.addToken('BACKTICK') - - while (this.peek() != '`' && this.isAnotherCharacter()) { - this.start = this.current - - if (this.peek() != '$' && this.peekNext() != '{' && !this.isAtEnd()) { - while ( - this.peek() != '$' && - this.peek() != '`' && - this.peekNext() != '{' && - !this.isAtEnd() - ) - this.advance() - - this.addToken( - 'TEMPLATE_LITERAL_TEXT', - this.sourceCode.substring(this.start, this.current) - ) - } else { - this.advance() // Consume the $ - this.advance() // Consume the { - this.addToken('DOLLAR_LEFT_BRACE') - this.start = this.current - - while (this.peek() != '}' && !this.isAtEnd()) { - this.start = this.current - this.scanToken() - } - - if (this.isAtEnd()) - this.error('MissingRightBraceToTerminatePlaceholder') - - this.start = this.current - this.advance() - this.addToken('RIGHT_BRACE') // Consume the } - } - } - - if (this.isAtEnd()) this.error('MissingBacktickToTerminateTemplateLiteral') - - this.start = this.current - this.advance() - this.addToken('BACKTICK') // Consume the closing ` - } - - /* - * For numbers, we consume any digits and a single decimal point, if present. - * We then add a token with the value of the number. - */ - private tokenizeNumber(): void { - while (this.isDigit(this.peek()) || this.peek() == '.') this.advance() - - const number = this.sourceCode.substring(this.start, this.current) - - // Guard against numbers with multiple decimal points (e.g. "1.2.4") - if (number.split('.').length > 2) { - const parts = number.split('.') - const suggestion = parts[0] + '.' + parts.slice(1).join('') - this.error('NumberWithMultipleDecimalPoints', { - suggestion: suggestion, - }) - } - - this.addToken('NUMBER', Number.parseFloat(number)) - } - - private tokenizeIdentifier(): void { - while (this.isAlphaNumeric(this.peek())) this.advance() - - const keywordType = Scanner.keywords[this.lexeme()] - if (keywordType) return this.addToken(keywordType) - - this.addToken('IDENTIFIER') - } - - private addSyntheticToken(type: TokenType, lexeme: string): void { - this.tokens.push({ - type, - lexeme: lexeme, - literal: null, - location: this.location(), - }) - } - - private addToken(type: TokenType, literal: any = null): void { - this.verifyEnabled(type, this.lexeme()) - - this.tokens.push({ - type, - lexeme: this.lexeme(), - literal, - location: this.location(), - }) - } - - private isAlpha(c: string): boolean { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' - } - - private isDigit(c: string): boolean { - return c >= '0' && c <= '9' - } - - private isAlphaNumeric(c: string): boolean { - return this.isAlpha(c) || this.isDigit(c) - } - - private isAtEnd(): boolean { - return this.current >= this.sourceCode.length - } - - // TODO: What is the purpose of these checks? - private isAnotherCharacter(): boolean { - const next = this.peek() - if (next == '\n') return false - if (next == '\0') return false - return true - } - - private shouldAddEOLToken(): boolean { - return ( - this.previouslyAddedToken() != null && - this.previouslyAddedToken() != 'EOL' - ) - } - - private advance(): string { - return this.sourceCode[this.current++] - } - - private peek(): string { - if (this.isAtEnd()) return '\0' - return this.sourceCode[this.current] - } - - private peekNext(): string { - if (this.current + 1 >= this.sourceCode.length) return '\0' - return this.sourceCode[this.current + 1] - } - - private previouslyAddedToken(): TokenType | null { - if (this.tokens.length === 0) return null - return this.tokens[this.tokens.length - 1].type - } - - private match(expected: string): boolean { - if (this.isAtEnd()) return false - if (this.sourceCode[this.current] != expected) return false - - this.current++ - return true - } - - private lexeme(): string { - return this.sourceCode.substring(this.start, this.current) - } - - private location(): Location { - return Location.fromLineOffset( - this.start + 1, - this.current + 1, - this.line, - this.lineOffset - ) - } - - private reset() { - this.tokens = [] - this.start = 0 - this.current = 0 - this.line = 1 - this.lineOffset = 0 - } - - private verifyEnabled(tokenType: TokenType, lexeme: string): void { - if (!this.languageFeatures) return - - if ( - this.languageFeatures.excludeList && - this.languageFeatures.excludeList.includes(tokenType) - ) - this.disabledLanguageFeatureError('ExcludeListViolation', { - excludeList: this.languageFeatures.excludeList, - tokenType, - lexeme, - }) - - if ( - this.languageFeatures.includeList && - !this.languageFeatures.includeList.includes(tokenType) - ) - this.disabledLanguageFeatureError('IncludeListViolation', { - includeList: this.languageFeatures.includeList, - tokenType, - lexeme, - }) - } - - private error(type: SyntaxErrorType, context: any = {}): never { - throw new SyntaxError( - translate(`error.syntax.${type}`, context), - this.location(), - type, - context - ) - } - - private disabledLanguageFeatureError( - type: DisabledLanguageFeatureErrorType, - context: any - ): never { - throw new DisabledLanguageFeatureError( - translate(`error.disabledLanguageFeature.${type}`, context), - this.location(), - type, - context - ) - } -} - -export function scan( - sourceCode: string, - ...args: [LanguageFeatures?] -): Token[] { - return new Scanner(...args).scanTokens(sourceCode) -} diff --git a/app/javascript/interpreter/languages/javascript/token.ts b/app/javascript/interpreter/languages/javascript/token.ts deleted file mode 100644 index 15e25a0f11..0000000000 --- a/app/javascript/interpreter/languages/javascript/token.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Location } from '../../location' - -export type TokenType = - // Single-character tokens - | 'BACKTICK' - | 'COLON' - | 'COMMA' - | 'LEFT_BRACE' - | 'LEFT_BRACKET' - | 'LEFT_PAREN' - | 'MINUS' - | 'PLUS' - | 'QUESTION_MARK' - | 'RIGHT_BRACE' - | 'RIGHT_BRACKET' - | 'RIGHT_PAREN' - | 'SLASH' - | 'STAR' - - // One, two or three character tokens. - | 'AMPERSAND_AMPERSAND' - | 'NOT' - | 'DOLLAR_LEFT_BRACE' - | 'EQUAL' - | 'GREATER_EQUAL' - | 'GREATER' - | 'LESS_EQUAL' - | 'LESS' - | 'MINUS_EQUAL' - | 'MINUS_MINUS' - | 'PIPE_PIPE' - | 'PLUS_PLUS' - | 'PLUS_EQUAL' - | 'SLASH_EQUAL' - | 'STAR_EQUAL' - - // Literals - | 'IDENTIFIER' - | 'NUMBER' - | 'STRING' - | 'TEMPLATE_LITERAL_TEXT' - - // Keywords - | 'AND' - | 'CONST' - | 'DO' - | 'ELSE' - | 'FALSE' - | 'FOR' - | 'FUNCTION' - | 'IF' - | 'IN' - | 'LET' - | 'NULL' - | 'OF' - | 'OR' - | 'RETURN' - | 'REPEAT_UNTIL_GAME_OVER' - | 'TRUE' - | 'WHILE' - - // Grouping tokens - | 'EQUALITY' - | 'STRICT_EQUALITY' - | 'INEQUALITY' - | 'STRICT_INEQUALITY' - - // Invisible tokens - | 'EOL' // End of statement - | 'EOF' // End of file - -export type Token = { - type: TokenType - lexeme: string - literal: any - location: Location -} diff --git a/app/javascript/interpreter/languages/jikiscript/error.ts b/app/javascript/interpreter/languages/jikiscript/error.ts deleted file mode 100644 index f4a85225b6..0000000000 --- a/app/javascript/interpreter/languages/jikiscript/error.ts +++ /dev/null @@ -1,69 +0,0 @@ -export type SyntaxErrorType = - | 'UnknownCharacter' - | 'MissingCommaAfterParameters' - | 'MissingDoToStartBlock' - | 'MissingEndAfterBlock' - | 'MissingConditionAfterIf' - | 'MissingDoubleQuoteToStartString' - | 'MissingDoubleQuoteToTerminateString' - | 'MissingFieldNameOrIndexAfterLeftBracket' - | 'MissingRightParenthesisAfterExpression' - | 'MissingRightBraceToTerminatePlaceholder' - | 'MissingBacktickToTerminateTemplateLiteral' - | 'MissingExpression' - | 'InvalidAssignmentTarget' - | 'ExceededMaximumNumberOfParameters' - | 'MissingEndOfLine' - | 'MissingFunctionName' - | 'MissingLeftParenthesisAfterFunctionName' - | 'MissingLeftParenthesisAfterFunctionCall' - | 'MissingParameterName' - | 'MissingRightParenthesisAfterParameters' - | 'MissingLeftParenthesisAfterWhile' - | 'MissingRightParenthesisAfterWhileCondition' - | 'MissingWhileBeforeDoWhileCondition' - | 'MissingLeftParenthesisAfterDoWhile' - | 'MissingRightParenthesisAfterDoWhileCondition' - | 'MissingVariableName' - | 'InvalidNumericVariableName' - | 'MissingConstantName' - | 'MissingToAfterVariableNameToInitializeValue' - | 'MissingToAfterVariableNameToChangeValue' - | 'MissingLeftParenthesisBeforeIfCondition' - | 'MissingRightParenthesisAfterIfCondition' - | 'MissingDoToStartFunctionBody' - | 'MissingDoToStartFunctionBody' - | 'MissingDoToStartIfBody' - | 'MissingDoToStartElseBody' - | 'MissingDoAfterRepeatStatementCondition' - | 'MissingDoAfterWhileStatementCondition' - | 'MissingLeftParenthesisAfterForeach' - | 'MissingLetInForeachCondition' - | 'MissingElementNameAfterForeach' - | 'MissingOfAfterElementNameInForeach' - | 'MissingRightParenthesisAfterForeachElement' - | 'MissingRightBracketAfterFieldNameOrIndex' - | 'MissingRightParenthesisAfterFunctionCall' - | 'MissingRightParenthesisAfterExpression' - | 'MissingRightParenthesisAfterExpressionWithPotentialTypo' - | 'MissingRightBracketAfterListElements' - | 'MissingRightBraceAfterMapElements' - | 'MissingWithBeforeParameters' - | 'MissingStringAsKey' - | 'MissingColonAfterKey' - | 'MissingFieldNameOrIndexAfterOpeningBracket' - | 'InvalidTemplateLiteral' - | 'MissingColonAfterThenBranchOfTernaryOperator' - | 'NumberEndsWithDecimalPoint' - | 'NumberWithMultipleDecimalPoints' - | 'NumberContainsAlpha' - | 'NumberStartsWithZero' - | 'UnexpectedElseWithoutIf' - | 'UnexpectedLiteralExpressionAfterIf' - | 'UnexpectedSpaceInIdentifier' - | 'UnexpectedVariableExpressionAfterIf' - | 'UnexpectedVariableExpressionAfterIfWithPotentialTypo' - | 'DuplicateParameterName' - | 'MissingTimesInRepeat' - | 'UnexpectedEqualsForAssignment' - | 'UnexpectedEqualsForEquality' diff --git a/app/javascript/interpreter/languages/jikiscript/token.ts b/app/javascript/interpreter/languages/jikiscript/token.ts deleted file mode 100644 index 12d91fffce..0000000000 --- a/app/javascript/interpreter/languages/jikiscript/token.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Location } from '../../location' - -export type TokenType = - // Single-character tokens - | 'BACKTICK' - | 'COLON' - | 'COMMA' - | 'LEFT_BRACE' - | 'LEFT_BRACKET' - | 'LEFT_PAREN' - | 'MINUS' - | 'PERCENT' - | 'PLUS' - | 'QUESTION_MARK' - | 'RIGHT_BRACE' - | 'RIGHT_BRACKET' - | 'RIGHT_PAREN' - | 'SLASH' - | 'STAR' - | 'NOT' - - // One, two or three character tokens. - | 'DOLLAR_LEFT_BRACE' - | 'GREATER_EQUAL' - | 'GREATER' - | 'LESS_EQUAL' - | 'LESS' - | 'EQUAL' - - // Literals - | 'IDENTIFIER' - | 'NUMBER' - | 'STRING' - | 'TEMPLATE_LITERAL_TEXT' - - // Keywords - | 'AND' - | 'CHANGE' - | 'DO' - | 'ELSE' - | 'END' - | 'FALSE' - | 'FOR' - | 'FOREACH' - | 'FUNCTION' - | 'IF' - | 'IN' - | 'NULL' - | 'IN' - | 'OR' - | 'REPEAT' - | 'REPEAT_UNTIL_GAME_OVER' - | 'RETURN' - | 'SET' - | 'TO' - | 'TIMES' - | 'TRUE' - | 'WHILE' - | 'WITH' - - // Grouping tokens - | 'STRICT_EQUALITY' - | 'STRICT_INEQUALITY' - - // Invisible tokens - | 'EOL' // End of statement - | 'EOF' // End of file - -export type Token = { - type: TokenType - lexeme: string - literal: any - location: Location -} diff --git a/app/javascript/interpreter/location.ts b/app/javascript/interpreter/location.ts index 28f0a0290b..5e267ba9d4 100644 --- a/app/javascript/interpreter/location.ts +++ b/app/javascript/interpreter/location.ts @@ -1,6 +1,6 @@ import { Expression } from './expression' import { Statement } from './statement' -import { type Token } from './/token' +import { type Token } from './token' export class Span { constructor(public begin: number, public end: number) {} diff --git a/app/javascript/interpreter/languages/jikiscript/parser.ts b/app/javascript/interpreter/parser.ts similarity index 97% rename from app/javascript/interpreter/languages/jikiscript/parser.ts rename to app/javascript/interpreter/parser.ts index f3d8f5b3ee..49de01ff88 100644 --- a/app/javascript/interpreter/languages/jikiscript/parser.ts +++ b/app/javascript/interpreter/parser.ts @@ -1,4 +1,4 @@ -import { SyntaxError } from '../../error' +import { SyntaxError } from './error' import { type SyntaxErrorType } from './error' import { ArrayExpression, @@ -17,9 +17,9 @@ import { TemplateLiteralExpression, TemplatePlaceholderExpression, TemplateTextExpression, -} from '../../expression' -import type { LanguageFeatures } from '../../interpreter' -import { Location } from '../../location' +} from './expression' +import type { LanguageFeatures } from './interpreter' +import { Location } from './location' import { Scanner } from './scanner' import { BlockStatement, @@ -34,15 +34,13 @@ import { Statement, VariableStatement, WhileStatement, -} from '../../statement' +} from './statement' import type { Token, TokenType } from './token' -import { translate } from '../../translator' -import { type Parser as GenericParser } from '../../interpreter' -import didYouMean from 'didyoumean' +import { translate } from './translator' import { isTypo } from './helpers/isTypo' import { errorForMissingDoAfterParameters } from './helpers/complexErrors' -export class Parser implements GenericParser { +export class Parser { private readonly scanner: Scanner private current: number = 0 private tokens: Token[] = [] @@ -293,7 +291,6 @@ export class Parser implements GenericParser { this.error('UnexpectedVariableExpressionAfterIf', ifToken.location) } - // console.log("condition", condition); } catch (e) { if (e instanceof SyntaxError && e.type == 'MissingExpression') { this.error('MissingConditionAfterIf', ifToken.location) @@ -309,10 +306,6 @@ export class Parser implements GenericParser { }) let elseBranch: Statement | null = null - // if(this.previous(2).type == "END") { - // console.log("Are we done twice?") - // // We're in a nested situation. We're done. - // } if (this.match('ELSE')) { if (this.match('IF')) { elseBranch = this.ifStatement() diff --git a/app/javascript/interpreter/resolver.ts b/app/javascript/interpreter/resolver.ts deleted file mode 100644 index bc8f380129..0000000000 --- a/app/javascript/interpreter/resolver.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { isString } from './checks' -import { SemanticError, type SemanticErrorType } from './error' -import { - ArrayExpression, - AssignExpression, - BinaryExpression, - CallExpression, - DictionaryExpression, - Expression, - type ExpressionVisitor, - GetExpression, - GroupingExpression, - LiteralExpression, - LogicalExpression, - SetExpression, - TemplateLiteralExpression, - TemplatePlaceholderExpression, - TemplateTextExpression, - TernaryExpression, - UnaryExpression, - UpdateExpression, - VariableExpression, -} from './expression' -import { Location } from './location' -import { - BlockStatement, - ConstantStatement, - DoWhileStatement, - ExpressionStatement, - ForeachStatement, - FunctionStatement, - IfStatement, - RepeatStatement, - RepeatUntilGameOverStatement, - ReturnStatement, - Statement, - type StatementVisitor, - VariableStatement, - WhileStatement, -} from './statement' -import type { Token } from './token' -import { translate } from './translator' - -type FunctionType = 'NONE' | 'FUNCTION' - -export class Resolver - implements ExpressionVisitor, StatementVisitor -{ - private readonly scopes: Map[] = [] - private readonly constants = new Set() - private currentFunction: FunctionType = 'NONE' - public readonly locals = new Map() - - constructor( - private allowVariableReassignment: boolean, - private globals: string[] - ) { - this.beginScope() - - for (const key of globals) { - this.declare(key) - this.define(key) - } - } - - public visitExpressionStatement(statement: ExpressionStatement): void { - this.resolve(statement.expression) - } - - public visitVariableStatement(statement: VariableStatement): void { - this.declare(statement.name) - this.resolve(statement.initializer) - this.define(statement.name) - } - - public visitConstantStatement(statement: ConstantStatement): void { - this.declareConstant(statement.name) - this.resolve(statement.initializer) - this.define(statement.name) - } - - public visitIfStatement(statement: IfStatement): void { - this.resolve(statement.condition) - this.resolve(statement.thenBranch) - if (statement.elseBranch !== null) this.resolve(statement.elseBranch) - } - - public visitRepeatStatement(statement: RepeatStatement): void { - this.resolve(statement.count) - this.resolve(statement.body) - } - - public visitRepeatUntilGameOverStatement( - statement: RepeatUntilGameOverStatement - ): void { - this.resolve(statement.body) - } - - public visitWhileStatement(statement: WhileStatement): void { - this.resolve(statement.condition) - this.resolve(statement.body) - } - - public visitDoWhileStatement(statement: DoWhileStatement): void { - this.resolve(statement.condition) - this.resolve(statement.body) - } - - public visitBlockStatement(statement: BlockStatement): void { - this.beginScope() - this.resolve(statement.statements) - this.endScope() - } - - public visitFunctionStatement(statement: FunctionStatement): void { - this.declare(statement.name) - this.define(statement.name) - this.resolveFunction(statement, 'FUNCTION') - } - - public visitReturnStatement(statement: ReturnStatement): void { - if (this.currentFunction === 'NONE') - this.error('TopLevelReturn', statement.location) - - if (statement.value !== null) this.resolve(statement.value) - } - - visitForeachStatement(statement: ForeachStatement): void { - this.resolve(statement.iterable) - this.beginScope() - this.declare(statement.elementName) - this.define(statement.elementName) - this.resolve(statement.body) - this.endScope() - } - - visitTemplateLiteralExpression(expression: TemplateLiteralExpression): void { - for (const part of expression.parts) this.resolve(part) - } - - visitTemplatePlaceholderExpression( - expression: TemplatePlaceholderExpression - ): void { - this.resolve(expression.inner) - } - - visitTemplateTextExpression(_expression: TemplateTextExpression): void {} - - visitArrayExpression(expression: ArrayExpression): void { - for (const element of expression.elements) this.resolve(element) - } - - visitDictionaryExpression(expression: DictionaryExpression): void { - for (const [_, value] of expression.elements) this.resolve(value) - } - - public visitCallExpression(expression: CallExpression): void { - this.resolve(expression.callee) - - for (const arg of expression.args) this.resolve(arg) - } - - public visitLiteralExpression(_expression: LiteralExpression): void {} - - public visitVariableExpression(expression: VariableExpression): void { - if (this.globals && this.globals[expression.name.lexeme]) return - - if ( - this.scopes.length > 0 && - this.scopes[this.scopes.length - 1].has(expression.name.lexeme) && - !this.scopes[this.scopes.length - 1].get(expression.name.lexeme) - ) - this.error('VariableUsedInOwnInitializer', expression.location, { - expression, - name: expression.name.lexeme, - }) - - this.resolveLocal(expression, expression.name) - } - - public visitUnaryExpression(expression: UnaryExpression): void { - this.resolve(expression.operand) - } - - public visitBinaryExpression(expression: BinaryExpression): void { - this.resolve(expression.left) - this.resolve(expression.right) - } - - public visitLogicalExpression(expression: LogicalExpression): void { - this.resolve(expression.left) - this.resolve(expression.right) - } - - public visitTernaryExpression(expression: TernaryExpression): void { - this.resolve(expression.condition) - this.resolve(expression.thenBranch) - this.resolve(expression.elseBranch) - } - - public visitGroupingExpression(expression: GroupingExpression): void { - this.resolve(expression.inner) - } - - public visitAssignExpression(expression: AssignExpression): void { - this.resolve(expression.value) - - if (this.constants.has(expression.name.lexeme)) - this.error('CannotAssignToConstant', expression.location) - - this.resolveLocal(expression, expression.name) - } - - public visitUpdateExpression(expression: UpdateExpression): void { - this.resolve(expression.operand) - - if (expression.operand instanceof VariableExpression) { - this.resolveLocal(expression.operand, expression.operand.name) - } else if (expression.operand instanceof GetExpression) { - this.resolve(expression.operand.obj) - } else if (expression.operand instanceof LiteralExpression) { - // Do nothing - } else this.error('InvalidPostfixOperand', expression.location) - } - - public visitGetExpression(expression: GetExpression): void { - this.resolve(expression.obj) - } - - public visitSetExpression(expression: SetExpression): void { - this.resolve(expression.obj) - this.resolve(expression.value) - } - - public resolve(element: Statement | Expression | Statement[]) { - if (element instanceof Statement || element instanceof Expression) { - element.accept(this) - return - } - - for (const statement of element) this.resolve(statement) - } - - private resolveLocal(expression: VariableExpression, name: Token) { - for (let i = this.scopes.length - 1; i >= 0; i--) { - if (this.scopes[i].has(name.lexeme)) { - if (this.constants.has(name.lexeme)) - this.error('CannotAssignToConstant', expression.location) - - this.setLocal(expression, this.scopes.length - 1 - i) - return - } - } - } - - private resolveFunction( - statement: FunctionStatement, - functionType: FunctionType - ) { - const enclosingFunction = this.currentFunction - this.currentFunction = functionType - - this.beginScope() - for (const param of statement.parameters) { - this.declare(param.name) - this.define(param.name) - } - this.resolve(statement.body) - this.endScope() - this.currentFunction = enclosingFunction - } - - private declareConstant(name: Token | string) { - this.constants.add(isString(name) ? name : name.lexeme) - this.declare(name) - } - - private declare(name: Token | string) { - if (this.scopes.length === 0) return - - const nameString = isString(name) ? name : name.lexeme - - if (this.allowVariableReassignment) { - if (this.scopes.find((scope) => scope.has(nameString))) { - return - } - } else { - const scope = this.scopes[this.scopes.length - 1] - if (scope.has(nameString)) { - this.error( - 'DuplicateVariableName', - isString(name) ? null : name.location, - { - name, - } - ) - } - } - - this.scopes[this.scopes.length - 1].set(nameString, false) - } - - private define(name: Token | string) { - if (this.scopes.length === 0) return - this.scopes[this.scopes.length - 1].set( - isString(name) ? name : name.lexeme, - true - ) - } - - private beginScope() { - this.scopes.push(new Map()) - } - - private endScope() { - this.scopes.pop() - } - - private setLocal(expression: Expression, depth: number): void { - this.locals.set(expression, depth) - } - - private error( - type: SemanticErrorType, - location: Location | null, - context: any = {} - ): never { - throw new SemanticError( - translate(`error.semantic.${type}`, context), - location, - type, - context - ) - } -} diff --git a/app/javascript/interpreter/languages/jikiscript/scanner.ts b/app/javascript/interpreter/scanner.ts similarity index 98% rename from app/javascript/interpreter/languages/jikiscript/scanner.ts rename to app/javascript/interpreter/scanner.ts index 341c13d31f..7ef929a57c 100644 --- a/app/javascript/interpreter/languages/jikiscript/scanner.ts +++ b/app/javascript/interpreter/scanner.ts @@ -20,12 +20,12 @@ import { DisabledLanguageFeatureError, type DisabledLanguageFeatureErrorType, SyntaxError, -} from '../../error' -import { type SyntaxErrorType } from './error' + SyntaxErrorType, +} from './error' import type { Token, TokenType } from './token' -import { Location } from '../../location' -import type { LanguageFeatures } from '../../interpreter' -import { translate } from '../../translator' +import { Location } from './location' +import type { LanguageFeatures } from './interpreter' +import { translate } from './translator' export class Scanner { private tokens: Token[] = [] diff --git a/app/javascript/interpreter/token.ts b/app/javascript/interpreter/token.ts index deff3b5cbf..df1a09e854 100644 --- a/app/javascript/interpreter/token.ts +++ b/app/javascript/interpreter/token.ts @@ -1,11 +1,74 @@ -import type { - Token as JikiscriptToken, - TokenType as JikiscriptTokenType, -} from './languages/jikiscript/token' -import type { - Token as JavascriptToken, - TokenType as JavascriptTokenType, -} from './languages/javascript/token' - -export type Token = JikiscriptToken | JavascriptToken -export type TokenType = JikiscriptTokenType | JavascriptTokenType +import { Location } from './location' + +export type TokenType = + // Single-character tokens + | 'BACKTICK' + | 'COLON' + | 'COMMA' + | 'LEFT_BRACE' + | 'LEFT_BRACKET' + | 'LEFT_PAREN' + | 'MINUS' + | 'PERCENT' + | 'PLUS' + | 'QUESTION_MARK' + | 'RIGHT_BRACE' + | 'RIGHT_BRACKET' + | 'RIGHT_PAREN' + | 'SLASH' + | 'STAR' + | 'NOT' + + // One, two or three character tokens. + | 'DOLLAR_LEFT_BRACE' + | 'GREATER_EQUAL' + | 'GREATER' + | 'LESS_EQUAL' + | 'LESS' + | 'EQUAL' + + // Literals + | 'IDENTIFIER' + | 'NUMBER' + | 'STRING' + | 'TEMPLATE_LITERAL_TEXT' + + // Keywords + | 'AND' + | 'CHANGE' + | 'DO' + | 'ELSE' + | 'END' + | 'FALSE' + | 'FOR' + | 'FOREACH' + | 'FUNCTION' + | 'IF' + | 'IN' + | 'NULL' + | 'IN' + | 'OR' + | 'REPEAT' + | 'REPEAT_UNTIL_GAME_OVER' + | 'RETURN' + | 'SET' + | 'TO' + | 'TIMES' + | 'TRUE' + | 'WHILE' + | 'WITH' + + // Grouping tokens + | 'STRICT_EQUALITY' + | 'STRICT_INEQUALITY' + + // Invisible tokens + | 'EOL' // End of statement + | 'EOF' // End of file + +export type Token = { + type: TokenType + lexeme: string + literal: any + location: Location +} diff --git a/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json b/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json index 0fbee11e98..715b59d5cb 100644 --- a/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/jumbled-house/config.json @@ -16,32 +16,32 @@ "checks": [ { "name": "getRectangleAt(20,50,60,40)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The frame of the house is not correct." }, { "name": "getTriangleAt(16,50, 50,30, 84,50)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The roof of the house is not at the correct position." }, { "name": "getRectangleAt(30,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left window frame isn't positioned correctly" }, { "name": "getRectangleAt(58,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right window frame isn't positioned correctly" }, { "name": "getRectangleAt(43,72,14,18)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door frame isn't positioned correctly" }, { "name": "getCircleAt(55,81,1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door knob isn't positiioned correctly" } ] diff --git a/bootcamp_content/projects/drawing/exercises/loops/config.json b/bootcamp_content/projects/drawing/exercises/loops/config.json deleted file mode 100644 index e5421eb0c0..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/config.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "title": "Loops", - "description": "Create 5 rectangles", - "project_type": "draw", - "level": 100, - "concepts": ["loops-repeat"], - "tests_type": "state", - "interpreter_options": { - "repeat_delay": 100 - }, - "tasks": [ - { - "name": "Draw 4 rectangles", - "tests": [ - { - "slug": "4-rectangles-20-20", - "name": "Draw 4 rectangles", - "function": "main", - "checks": [ - { - "name": "numElements()", - "value": 4, - "error_html": "Expected 4 rectangles to be drawn, but only got %actual%." - }, - { - "name": "getRectAt(20,20,20,20)", - "matcher": "toExist", - "error_html": "No rectangle at (20,20,20,20)" - } - ] - }, - { - "slug": "4-rectangles-40-40", - "name": "Draw 4 rectangles x2", - "description": "Draw 4 rectangles at (40,40,20,20)", - "function": "main", - "checks": [ - { - "name": "numElements()", - "value": 4, - "error_html": "Expected 4 rectangles to be drawn, but only got %actual%." - }, - { - "name": "getRectAt(40,40,20,20)", - "matcher": "toExist", - "error_html": "We couldn't find a rectangle at (40,40,20,20). Check that you've got the co-ordinates, width and height all correct and try again!" - } - ] - } - ] - } - ] -} diff --git a/bootcamp_content/projects/drawing/exercises/loops/example.jiki b/bootcamp_content/projects/drawing/exercises/loops/example.jiki deleted file mode 100644 index 802eb8f278..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/example.jiki +++ /dev/null @@ -1,7 +0,0 @@ -function two_fer with name do - if name is "" do - return "One for you, one for me." - else do - return "One for " + name + ", one for me." - end -end diff --git a/bootcamp_content/projects/drawing/exercises/loops/introduction.md b/bootcamp_content/projects/drawing/exercises/loops/introduction.md deleted file mode 100644 index a8c9ace4f3..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/introduction.md +++ /dev/null @@ -1,15 +0,0 @@ -# TwoFer Part 1 - -Two Fer is a classic Exercism exercise. -Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_. - -In some English accents, when you say "two for" quickly, it sounds like "two fer". Two-for-one is a way of saying that if you buy one, you also get one for free. So the phrase "two-fer" often implies a two-for-one offer. - -Imagine a bakery that has a holiday offer where you can buy two cookies for the price of one ("two-fer one!"). You take the offer and (very generously) decide to give the extra cookie to someone else in the queue. - -As you give them the cookie, you one of two things. - -- If you know their name, you say: "One for <name>, one for me." -- If you don't know their name, you say: "One for you, one for me." - -For example, you might say "One for Jeremy, one for me" if you know Jeremy's name. diff --git a/bootcamp_content/projects/drawing/exercises/loops/stub.jiki b/bootcamp_content/projects/drawing/exercises/loops/stub.jiki deleted file mode 100644 index 20a0e40b8a..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/stub.jiki +++ /dev/null @@ -1,4 +0,0 @@ -rect(rand(0,10),10,10,10) -rect(rand(0,80),10,10,10) -rect(rand(0,80),10,10,10) -rect(rand(0,80),10,10,10) \ No newline at end of file diff --git a/bootcamp_content/projects/drawing/exercises/loops/task-1.md b/bootcamp_content/projects/drawing/exercises/loops/task-1.md deleted file mode 100644 index 1b03934503..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/task-1.md +++ /dev/null @@ -1,5 +0,0 @@ -We've given you a function skeleton that takes one input - `name`. - -It will either be an empty string (`""`) or it will be someone's name (`"Jeremy"`). - -Let's start off by just considering that empty version and always returning our default version `"One for me, one for you."`. diff --git a/bootcamp_content/projects/drawing/exercises/loops/task-2.md b/bootcamp_content/projects/drawing/exercises/loops/task-2.md deleted file mode 100644 index 2a597efd6a..0000000000 --- a/bootcamp_content/projects/drawing/exercises/loops/task-2.md +++ /dev/null @@ -1,7 +0,0 @@ -Nice work! - -Now we need to handle the situation where we **do** know the person's name. - -Sometime's the name will be empty (`""`) in which case we want to continue returning `"One for you, one for me."`, but other times `name` will contain a name, in which case we want to include it in the return value (e.g. `"One for Jeremy, one for me."`). - -Remember, you can join multiple strings together using the `join_strings(...)` function. diff --git a/bootcamp_content/projects/drawing/exercises/penguin/config.json b/bootcamp_content/projects/drawing/exercises/penguin/config.json index e780982b7a..9e5397642b 100644 --- a/bootcamp_content/projects/drawing/exercises/penguin/config.json +++ b/bootcamp_content/projects/drawing/exercises/penguin/config.json @@ -18,87 +18,87 @@ "checks": [ { "name": "getRectangleAt(0, 0, 100, 100)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sky has gone wrong." }, { "name": "getRectangleAt(0, 70, 100, 30)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ground has gone wrong." }, { "name": "getEllipseAt(28, 55, 10, 25)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left wing doesn't seem right." }, { "name": "getEllipseAt(72, 55, 10, 25)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right wing doesn't seem right." }, { "name": "getEllipseAt(50, 53, 25, 40)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The outer body has gone wrong." }, { "name": "getEllipseAt(50, 50, 21, 39)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The inner body has gone wrong." }, { "name": "getCircleAt(50, 31, 23)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The head has gone wrong." }, { "name": "getEllipseAt(41, 32, 11, 14)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left side of the face doesn't look right." }, { "name": "getEllipseAt(59, 32, 11, 14)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right side of the face doesn't look right." }, { "name": "getEllipseAt(50, 40, 16, 11)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The lower part of the face doesn't look right." }, { "name": "getCircleAt(42, 33, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left eye seems off." }, { "name": "getCircleAt(43, 34, 1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left iris seems off." }, { "name": "getCircleAt(58, 33, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right eye seems off." }, { "name": "getCircleAt(57, 34, 1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right iris seems off." }, { "name": "getEllipseAt(40, 93, 7, 4)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left foot's gone astray." }, { "name": "getEllipseAt(60, 93, 7, 4)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right foot's not right." }, { "name": "getTriangleAt(46, 38, 54, 38, 50, 47)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The nose isn't right." } ] diff --git a/bootcamp_content/projects/drawing/exercises/rainbow-ball/config.json b/bootcamp_content/projects/drawing/exercises/rainbow-ball/config.json index 1c416ff8e5..4cdcc512d0 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow-ball/config.json +++ b/bootcamp_content/projects/drawing/exercises/rainbow-ball/config.json @@ -21,12 +21,12 @@ "checks": [ { "name": "getCircleAt(5, 5, 10)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first circle is not right." }, { "name": "getCircleAt(7, 6, 10)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The second circle is not right." }, { diff --git a/bootcamp_content/projects/drawing/exercises/rainbow/config.json b/bootcamp_content/projects/drawing/exercises/rainbow/config.json index 830bee2f54..cdc207994d 100644 --- a/bootcamp_content/projects/drawing/exercises/rainbow/config.json +++ b/bootcamp_content/projects/drawing/exercises/rainbow/config.json @@ -21,12 +21,12 @@ "checks": [ { "name": "getRectangleAt(1, 0, undefined, 100)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first rectangle is missing" }, { "name": "getRectangleAt(99, 0, undefined, 100)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The last rectangle is missing" }, { diff --git a/bootcamp_content/projects/drawing/exercises/sleepy-house/config.json b/bootcamp_content/projects/drawing/exercises/sleepy-house/config.json index e179a26103..31c8443560 100644 --- a/bootcamp_content/projects/drawing/exercises/sleepy-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/sleepy-house/config.json @@ -16,32 +16,32 @@ "checks": [ { "name": "getRectangleAt(20,50,60,40)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The frame of the house is not correct - you shouldn't need to move this in this exercise." }, { "name": "getTriangleAt(16,50, 50,30, 84,50)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The roof of the house is not at the correct position - you shouldn't need to move this in this exercise." }, { "name": "getRectangleAt(30,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left window frame isn't positioned correctly - you shouldn't need to move this in this exercise." }, { "name": "getRectangleAt(58,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right window frame isn't positioned correctly - you shouldn't need to move this in this exercise." }, { "name": "getRectangleAt(43,72,14,18)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door frame isn't positioned correctly - you shouldn't need to move this in this exercise." }, { "name": "getCircleAt(55,81,1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door knob isn't positiioned correctly - you shouldn't need to move this in this exercise." } ] diff --git a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json index f5cd53d727..d89ad012e8 100644 --- a/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json +++ b/bootcamp_content/projects/drawing/exercises/sprouting-flower/config.json @@ -21,53 +21,53 @@ "checks": [ { "name": "getCircleAt(50, 89, 0.4)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first Flower Head isn't correct." }, { "name": "getCircleAt(50, 30, 24)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The final Flower Head isn't correct." }, { "name": "getCircleAt(50, 89, 0.1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first Pistil isn't correct." }, { "name": "getCircleAt(50, 30, 6)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The final Pistil isn't correct." }, { "name": "getRectangleAt(49.95, 89, 0.1, 1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first Stem isn't correct." }, { "name": "getRectangleAt(47, 30, 6, 60)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The final Stem isn't correct." }, { "name": "getEllipseAt(49.75, 89.5, 0.2, 0.08)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first Left Leaf isn't correct." }, { "name": "getEllipseAt(35, 60, 12, 4.8)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The final Left Leaf isn't correct." }, { "name": "getEllipseAt(50.25, 89.5, 0.2, 0.08)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first Right Leaf isn't correct." }, { "name": "getEllipseAt(65, 60, 12, 4.8)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The final Right Leaf isn't correct." } ] diff --git a/bootcamp_content/projects/drawing/exercises/structured-house/config.json b/bootcamp_content/projects/drawing/exercises/structured-house/config.json index adf9e72842..755a4c3761 100644 --- a/bootcamp_content/projects/drawing/exercises/structured-house/config.json +++ b/bootcamp_content/projects/drawing/exercises/structured-house/config.json @@ -16,32 +16,32 @@ "checks": [ { "name": "getRectangleAt(20,50,60,40)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The frame of the house is not correct." }, { "name": "getTriangleAt(16,50, 50,30, 84,50)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The roof of the house is not at the correct position." }, { "name": "getRectangleAt(30,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The left window frame isn't positioned correctly" }, { "name": "getRectangleAt(58,55,12,13)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The right window frame isn't positioned correctly" }, { "name": "getRectangleAt(43,72,14,18)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door frame isn't positioned correctly" }, { "name": "getCircleAt(55,81,1)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The door knob isn't positiioned correctly" }, { diff --git a/bootcamp_content/projects/drawing/exercises/sunset/config.json b/bootcamp_content/projects/drawing/exercises/sunset/config.json index d37eb0e2c9..1e73b61ec5 100644 --- a/bootcamp_content/projects/drawing/exercises/sunset/config.json +++ b/bootcamp_content/projects/drawing/exercises/sunset/config.json @@ -21,17 +21,17 @@ "checks": [ { "name": "getCircleAt(50, 11, 5.2)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sun seems wrong near the beginning." }, { "name": "getCircleAt(50, 20, 7)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sun seems wrong near the middle." }, { "name": "getCircleAt(50, 109, 24.8)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sun seems wrong near the end." }, { diff --git a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json index 2acbb2b05e..ba6e92f099 100644 --- a/bootcamp_content/projects/golf/exercises/rolling-ball/config.json +++ b/bootcamp_content/projects/golf/exercises/rolling-ball/config.json @@ -29,22 +29,22 @@ "checks": [ { "name": "getCircleAt(27, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the left." }, { "name": "getCircleAt(29, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to start in the right place." }, { "name": "getCircleAt(88, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the hole." }, { "name": "getCircleAt(89, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the right." } ] diff --git a/bootcamp_content/projects/golf/exercises/shot-checker/config.json b/bootcamp_content/projects/golf/exercises/shot-checker/config.json index 04145138e4..0dfd16f472 100644 --- a/bootcamp_content/projects/golf/exercises/shot-checker/config.json +++ b/bootcamp_content/projects/golf/exercises/shot-checker/config.json @@ -30,22 +30,22 @@ "checks": [ { "name": "getCircleAt(29, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the left." }, { "name": "getCircleAt(30, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to start in the right place." }, { "name": "getCircleAt(53, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(54, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the right." }, { @@ -71,27 +71,27 @@ "checks": [ { "name": "getCircleAt(29, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the left." }, { "name": "getCircleAt(30, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to start in the right place." }, { "name": "getCircleAt(100, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(101, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the right." }, { "name": "getCircleAt(100, 76, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball tried to sink into the ground beyond the hole." }, { @@ -117,32 +117,32 @@ "checks": [ { "name": "getCircleAt(29, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the left." }, { "name": "getCircleAt(30, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to start in the right place." }, { "name": "getCircleAt(86, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to roll the correct length." }, { "name": "getCircleAt(86, 83, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(86, 84, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(86, 85, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to sink into the grass too far!" }, { @@ -168,32 +168,32 @@ "checks": [ { "name": "getCircleAt(29, 75, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to go too far to the left." }, { "name": "getCircleAt(30, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to start in the right place." }, { "name": "getCircleAt(93, 75, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to roll the correct length." }, { "name": "getCircleAt(93, 83, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(93, 84, 3)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The ball doesn't seem to reach the correct finishing point." }, { "name": "getCircleAt(93, 85, 3)", - "matcher": "toNotExist", + "matcher": "toBeUndefined", "error_html": "The ball seems to sink into the grass too far!" }, { diff --git a/bootcamp_content/projects/maze/exercises/implement-move/config.json b/bootcamp_content/projects/maze/exercises/implement-move/config.json deleted file mode 100644 index 6a3b694b20..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/config.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "title": "Implement move", - "description": "Implement the move function", - "project_type": "maze", - "level": 100, - "tests_type": "state", - "tasks": [ - { - "name": "Move up", - "tests": [ - { - "slug": "move-up", - "name": "Moves up", - "setup_functions": [ - [ - "setupGrid", - [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0] - ] - ], - ["setupDirection", ["up"]], - ["setupPosition", [1, 1]] - ], - "function": "move", - "available_functions": ["moveCharacter"], - "checks": [ - { - "name": "position", - "value": [0, -1] - } - ] - } - ] - }, - { - "name": "Move down", - "tests": [ - { - "slug": "move-down", - "name": "Moves down", - "setup_functions": [ - [ - "setupGrid", - [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0] - ] - ], - ["setupDirection", ["down"]], - ["setupPosition", [1, 1]] - ], - "function": "move", - "checks": [ - { - "name": "position", - "value": [0, 1] - } - ] - } - ] - }, - { - "name": "Move left", - "tests": [ - { - "slug": "move-left", - "name": "Moves left", - - "setup_functions": [ - [ - "setupGrid", - [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0] - ] - ], - ["setupDirection", ["left"]], - ["setupPosition", [1, 1]] - ], - "function": "move", - "checks": [ - { - "name": "position", - "value": [-1, 0] - } - ] - } - ] - }, - { - "name": "Move right", - "tests": [ - { - "slug": "move-right", - "name": "Moves right", - "setup_functions": [ - [ - "setupGrid", - [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0] - ] - ], - ["setupDirection", ["left"]], - ["setupPosition", [1, 1]] - ], - "function": "move", - "checks": [ - { - "name": "position", - "value": [1, 0] - } - ] - } - ] - } - ] -} diff --git a/bootcamp_content/projects/maze/exercises/implement-move/example.jiki b/bootcamp_content/projects/maze/exercises/implement-move/example.jiki deleted file mode 100644 index a43e2c22f8..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/example.jiki +++ /dev/null @@ -1,21 +0,0 @@ -// You have two tools at your disposal -// - A variable called direction. It's been defined like this. -// `set direction to "down"` -// - A function called moveCharacter, that expects an x and y coordinate as its input, and moves the character accordingly. - -// Your job is to check the direction then call moveCharacter with the relative direction, so for example if you move up, you call moveCharacter(0,-1) - no change to the x, but a negative change to the y coordinate. - -function move do - if direction is "up" do - moveCharacter(0, -1) - end - else if direction is "down" do - moveCharacter(0, 1) - end - else if direction is "right" do - moveCharacter(1, 0) - end - else if direction is "left" do - moveCharacter(-1, 0) - end -end \ No newline at end of file diff --git a/bootcamp_content/projects/maze/exercises/implement-move/introduction.md b/bootcamp_content/projects/maze/exercises/implement-move/introduction.md deleted file mode 100644 index da2e8f32a2..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/introduction.md +++ /dev/null @@ -1,19 +0,0 @@ -# Even or odd - -Let's build a function that takes a _number_ as an input and returns a _string_ specifying whether it's `"Even"` (0, 2, 4, 6, 8, etc), or `"Odd"` (1, 3, 5, 7, etc) or `"Zero"`. - -To approach this problem, think about what it is that acutally makes a number odd or even. - -
- Totally Stuck? - A good way of working out if a number is even or odd is to check whether it has a remainder when it's divided by 2. - -You probably remember from school that a remainder is what’s left over when you divide a number but can’t divide it evenly. In other words, it’s the part of the number that doesn’t fit into equal groups. - -For example, if you divide 7 by 3, you can fit two groups of 3 into 7 (since 3 + 3 = 6), but there’s 1 left over. That leftover 1 is the remainder. And that remainder makes it an odd number. - -So to solve this exercise, you might like to use the **[remainder operator]()**. - -``` -
-``` diff --git a/bootcamp_content/projects/maze/exercises/implement-move/stub.jk b/bootcamp_content/projects/maze/exercises/implement-move/stub.jk deleted file mode 100644 index d358c9aab8..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/stub.jk +++ /dev/null @@ -1,5 +0,0 @@ -// Receives a number as its input -// and should return "Positive", "Negative" or "Zero" -function even_or_odd with num do - -end \ No newline at end of file diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-1.md b/bootcamp_content/projects/maze/exercises/implement-move/task-1.md deleted file mode 100644 index ebd3b8b008..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/task-1.md +++ /dev/null @@ -1,3 +0,0 @@ -# Task 1 - -Start with trying to get the `"Even"` check in place. diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-2.md b/bootcamp_content/projects/maze/exercises/implement-move/task-2.md deleted file mode 100644 index a5378b687d..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/task-2.md +++ /dev/null @@ -1,3 +0,0 @@ -# Task 2 - -Great! Now get the `"Odd"` check in place. diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-3.md b/bootcamp_content/projects/maze/exercises/implement-move/task-3.md deleted file mode 100644 index da38a007cc..0000000000 --- a/bootcamp_content/projects/maze/exercises/implement-move/task-3.md +++ /dev/null @@ -1,3 +0,0 @@ -# Task 3 - -Great! Finally, let's check that `0` is returned as `Even`. diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json index ff1998b56b..9e9e09b27d 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json @@ -4,6 +4,7 @@ "concepts": ["strings-using", "conditionals"], "level": 4, "idx": 1, + "tests_type": "io", "tasks": [ { "name": "Correctly identify even numbers", diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json index 3e775089b0..a990ea34cb 100644 --- a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json +++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json @@ -4,6 +4,7 @@ "concepts": ["strings-using", "conditionals"], "level": 4, "idx": 1, + "tests_type": "io", "tasks": [ { "name": "Correctly identify positive numbers", diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/determine-winner/config.json b/bootcamp_content/projects/rock-paper-scissors/exercises/determine-winner/config.json index 8165df6136..1837b30871 100644 --- a/bootcamp_content/projects/rock-paper-scissors/exercises/determine-winner/config.json +++ b/bootcamp_content/projects/rock-paper-scissors/exercises/determine-winner/config.json @@ -21,7 +21,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -40,7 +40,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -59,7 +59,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -78,7 +78,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -97,7 +97,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -116,7 +116,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -135,7 +135,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -154,7 +154,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { @@ -173,7 +173,7 @@ "checks": [ { "name": "result", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "You didn't announce a result!" }, { diff --git a/bootcamp_content/projects/time/exercises/digital-clock/config.json b/bootcamp_content/projects/time/exercises/digital-clock/config.json index 80c87ee734..7cdc7aa74a 100644 --- a/bootcamp_content/projects/time/exercises/digital-clock/config.json +++ b/bootcamp_content/projects/time/exercises/digital-clock/config.json @@ -18,7 +18,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { @@ -37,7 +37,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { @@ -61,7 +61,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { @@ -80,7 +80,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { @@ -104,7 +104,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { @@ -127,7 +127,7 @@ "checks": [ { "name": "displayedTime", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The clock didn't get updated. Make sure you use the `display_time` function." }, { diff --git a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json index e4097adf34..bfc67db568 100644 --- a/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json +++ b/bootcamp_content/projects/weather/exercises/cloud-rain-sun/config.json @@ -26,62 +26,62 @@ "checks": [ { "name": "getCircleAt(75, 30, 15)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sun isn't correct." }, { "name": "getRectangleAt(25, undefined, 50, undefined)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The base of the cloud isn't correct." }, { "name": "getCircleAt(25, 50, 10)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fluffy bits of the cloud aren't correct." }, { "name": "getCircleAt(40, 40, 15)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fluffy bits of the cloud aren't correct." }, { "name": "getCircleAt(55, 40, 20)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fluffy bits of the cloud aren't correct." }, { "name": "getCircleAt(75, 50, 10)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fluffy bits of the cloud aren't correct." }, { "name": "getCircleAt(75, 50, 10)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fluffy bits of the cloud aren't correct." }, { "name": "getEllipseAt(30, 70, 3, 5)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The first rain drop isn't correct" }, { "name": "getEllipseAt(50, 70, 3, 5)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The second rain drop isn't correct" }, { "name": "getEllipseAt(70, 70, 3, 5)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The third rain drop isn't correct" }, { "name": "getEllipseAt(40, 80, 3, 5)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fourth rain drop isn't correct" }, { "name": "getEllipseAt(60, 80, 3, 5)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The fifth rain drop isn't correct" } ] diff --git a/bootcamp_content/projects/weather/exercises/sunshine/config.json b/bootcamp_content/projects/weather/exercises/sunshine/config.json index af00163cdd..11ab9494e0 100644 --- a/bootcamp_content/projects/weather/exercises/sunshine/config.json +++ b/bootcamp_content/projects/weather/exercises/sunshine/config.json @@ -18,7 +18,7 @@ "checks": [ { "name": "getCircleAt(50, 50, 25)", - "matcher": "toExist", + "matcher": "toBeDefined", "error_html": "The sun isn't correct." } ] diff --git a/test/javascript/interpreter/ihid.test.ts b/test/javascript/interpreter/ihid.test.ts deleted file mode 100644 index 011bbcf47b..0000000000 --- a/test/javascript/interpreter/ihid.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { scan } from '@/interpreter/languages/javascript/scanner' -import type { TokenType } from '@/interpreter/languages/javascript/token' - -describe('single-character', () => { - test.each([ - ['{', 'LEFT_BRACE'], - ['[', 'LEFT_BRACKET'], - ['(', 'LEFT_PAREN'], - ['}', 'RIGHT_BRACE'], - [']', 'RIGHT_BRACKET'], - [')', 'RIGHT_PAREN'], - [':', 'COLON'], - [',', 'COMMA'], - ['-', 'MINUS'], - ['+', 'PLUS'], - ['*', 'STAR'], - ['/', 'SLASH'], - // ["?", "QUESTION_MARK"], - ])("'%s' token", (source: string, expectedType: string) => { - const tokens = scan(source) - expect(tokens[0].type).toBe(expectedType as TokenType) - expect(tokens[0].lexeme).toBe(source) - expect(tokens[0].literal).toBeNull - }) -}) diff --git a/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts b/test/javascript/interpreter/interpreter.test.ts similarity index 95% rename from test/javascript/interpreter/languages/jikiscript/interpreter.test.ts rename to test/javascript/interpreter/interpreter.test.ts index b5fc4ba655..0b6a99ee4e 100644 --- a/test/javascript/interpreter/languages/jikiscript/interpreter.test.ts +++ b/test/javascript/interpreter/interpreter.test.ts @@ -1,10 +1,9 @@ import { Interpreter, - interpretJikiScript as interpret, - evaluateJikiScriptFunction as evaluateFunction, + interpret, + evaluateFunction, } from '@/interpreter/interpreter' import type { ExecutionContext } from '@/interpreter/executor' -import { error } from 'jquery' describe('statements', () => { describe('expression', () => { @@ -129,43 +128,17 @@ describe('statements', () => { expect(frames[0].variables).toMatchObject({ x: true }) }) - describe('truthiness', () => { - describe('enabled', () => { - test('and', () => { - const { frames } = interpret('set x to [] and true', { - languageFeatures: { truthiness: 'ON' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - - test('or', () => { - const { frames } = interpret('set x to 0 or false', { - languageFeatures: { truthiness: 'ON' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) + describe("truthiness doesn't exit", () => { + test('and', () => { + const { frames } = interpret('set x to true and []') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('ERROR') }) - describe('disabled', () => { - test('and', () => { - const { frames } = interpret('set x to true and []', { - languageFeatures: { truthiness: 'OFF' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('ERROR') - }) - - test('or', () => { - const { frames } = interpret('set x to false or 0', { - languageFeatures: { truthiness: 'OFF' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('ERROR') - }) + test('or', () => { + const { frames } = interpret('set x to false or 0') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('ERROR') }) }) }) @@ -1063,14 +1036,16 @@ describe('evaluateFunction', () => { expect(frames).toBeArrayOfSize(1) }) - test('idempotent', () => { + test('idempotent - 1', () => { const code = ` set x to 1 function move do change x to x + 1 return x end` - const interpreter = new Interpreter(code, { language: 'JikiScript' }) + const interpreter = new Interpreter(code, { + languageFeatures: { allowGlobals: true }, + }) interpreter.compile() const { value: value1 } = interpreter.evaluateFunction('move') const { value: value2 } = interpreter.evaluateFunction('move') @@ -1078,6 +1053,22 @@ describe('evaluateFunction', () => { expect(value2).toBe(2) }) + test('idempotent - 2', () => { + const code = ` + set x to 1 + function move do + change x to x + 1 + return x + end + move() + move() + ` + const { frames, error } = interpret(code, { + languageFeatures: { allowGlobals: true }, + }) + expect(frames[6].variables.x).toBe(3) + }) + // TODO: Work out all this syntax test.skip('full program', () => { const code = ` @@ -1140,15 +1131,6 @@ describe('errors', () => { expect(error!.context).toBeNull }) - test('resolver', () => { - const { frames, error } = interpret('return 1') - expect(frames).toBeEmpty() - expect(error).not.toBeNull() - expect(error!.category).toBe('SemanticError') - expect(error!.type).toBe('TopLevelReturn') - expect(error!.context).toBeNull - }) - describe('runtime', () => { describe('evaluateFunction', () => { test('first frame', () => { @@ -1358,6 +1340,24 @@ describe('errors', () => { describe('suggestions', () => { test('function name differs by one letter', () => { + const code = ` + function move do + end + m0ve() + ` + const { frames, error } = evaluateFunction(code, {}, 'move') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].error).not.toBeNull() + expect(frames[0].error!.context).toMatchObject({ + didYouMean: { + function: 'move', + variable: null, + }, + }) + }) + + // Recursion isn't supported in JikiScript (yet?) + test.skip('recursive function name differs by one letter', () => { const code = ` function move do m0ve() diff --git a/test/javascript/interpreter/languages/javascript/interpreter.test.ts b/test/javascript/interpreter/languages/javascript/interpreter.test.ts deleted file mode 100644 index e17d32123f..0000000000 --- a/test/javascript/interpreter/languages/javascript/interpreter.test.ts +++ /dev/null @@ -1,1545 +0,0 @@ -import { - Interpreter, - interpretJavaScript as interpret, - evaluateJavaScriptFunction as evaluateFunction, -} from '@/interpreter/interpreter' -import type { ExecutionContext } from '@/interpreter/executor' - -describe('statements', () => { - describe('expression', () => { - test('number', () => { - const { frames, error } = interpret('let x = 1') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 1 }) - }) - - test('string', () => { - const { frames } = interpret('let x = "hello there"') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 'hello there' }) - }) - - describe('unary', () => { - test('negation', () => { - const { frames } = interpret('let x = !true') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - - test('minus', () => { - const { frames } = interpret('let x = -3') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: -3 }) - }) - - describe('increment', () => { - test('variable', () => { - const { frames } = interpret(` - let x = 3 - x++ - `) - expect(frames).toBeArrayOfSize(2) - 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: 4 }) - }) - - test('array', () => { - const { frames } = interpret(` - let x = [1,2] - x[1]++ - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: [1, 2] }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: [1, 3] }) - }) - - test('dictionary', () => { - const { frames } = interpret(` - let x = {"count":1} - x["count"]++ - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: { count: 1 } }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: { count: 2 } }) - }) - }) - - describe('decrement', () => { - test('variable', () => { - const { frames } = interpret(` - let x = 3 - x-- - `) - expect(frames).toBeArrayOfSize(2) - 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: 2 }) - }) - - test('array', () => { - const { frames } = interpret(` - let x = [1,2] - x[1]-- - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: [1, 2] }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: [1, 1] }) - }) - - test('dictionary', () => { - const { frames } = interpret(` - let x = {"count":1} - x["count"]-- - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: { count: 1 } }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: { count: 0 } }) - }) - }) - }) - - describe('binary', () => { - describe('arithmetic', () => { - test('plus', () => { - const { frames } = interpret('let x = 2 + 3') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 5 }) - }) - - test('minus', () => { - const { frames } = interpret('let x = 7 - 6') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 1 }) - }) - - test('division', () => { - const { frames } = interpret('let x = 20 / 5') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 4 }) - }) - - test('multiplication', () => { - const { frames } = interpret('let x = 4 * 2') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 8 }) - }) - }) - - describe('comparison', () => { - test('equality', () => { - const { frames } = interpret('let x = 2 == "2"') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - - test('strict equality', () => { - const { frames, error } = interpret('let x = 2 === "2"') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - - test('inequality', () => { - const { frames } = interpret('let x = 2 != "2"') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - - test('strict inequality', () => { - const { frames } = interpret('let x = 2 !== "2"') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - }) - - describe('logical', () => { - test('and', () => { - const { frames } = interpret('let x = true and false') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - - test('&&', () => { - const { frames } = interpret('let x = true && false') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - - test('or', () => { - const { frames } = interpret('let x = true or false') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - - test('&&', () => { - const { frames } = interpret('let x = true || false') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - - describe('truthiness', () => { - describe('enabled', () => { - test('and', () => { - const { frames } = interpret('let x = [] and true', { - languageFeatures: { truthiness: 'ON' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: true }) - }) - - test('or', () => { - const { frames } = interpret('let x = 0 or false', { - languageFeatures: { truthiness: 'ON' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: false }) - }) - }) - - describe('disabled', () => { - test('and', () => { - const { frames } = interpret('let x = true and []', { - languageFeatures: { truthiness: 'OFF' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('ERROR') - }) - - test('or', () => { - const { frames } = interpret('let x = false or 0', { - languageFeatures: { truthiness: 'OFF' }, - }) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('ERROR') - }) - }) - }) - }) - - describe('strings', () => { - test('plus', () => { - const { frames } = interpret('let x = "sw" + "eet" ') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 'sweet' }) - }) - }) - - describe('template literals', () => { - test('text only', () => { - const { frames } = interpret('let x = `hello`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 'hello' }) - }) - - test('placeholder only', () => { - const { frames } = interpret('let x = `${3*4}`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: '12' }) - }) - - test('string', () => { - const { frames } = interpret('let x = `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(` - let x = 1 - let y = \`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('let x = `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( - 'let x = `${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('ternary', () => { - test('then branch', () => { - const { frames } = interpret('let x = true ? 1 : 2') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 1 }) - }) - - test('else branch', () => { - const { frames } = interpret('let x = false ? 1 : 2') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - }) - - test('nested', () => { - const { frames } = interpret( - 'let x = false ? 1 : false ? 2 : true ? 3 : 4' - ) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 3 }) - }) - }) - - describe('get', () => { - describe('dictionary', () => { - test('single field', () => { - const { frames } = interpret(` - let movie = {"title": "The Matrix"} - let title = 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(` - let movie = {"director": {"name": "Peter Jackson"}} - let name = 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(` - let scores = [7, 3, 10] - let latest = 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(` - let scoreMinMax = [[3, 7], [1, 6]] - let secondMin = 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(` - let movie = {"title": "The Matrix"} - movie["title"] = "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(` - let movie = {"director": {"name": "Peter Jackson"}} - movie["director"]["name"] = "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(` - let scores = [7, 3, 10] - scores[2] = 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(` - let scoreMinMax = [[3, 7], [1, 6]] - scoreMinMax[1][0] = 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', () => { - test('regular', () => { - const { frames } = interpret(` - let x = 2 - x = 3 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 3 }) - }) - - describe('compound', () => { - test('plus', () => { - const { frames } = interpret(` - let x = 2 - x += 3 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 5 }) - }) - - test('minus', () => { - const { frames } = interpret(` - let x = 2 - x -= 3 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: -1 }) - }) - - test('multiply', () => { - const { frames } = interpret(` - let x = 2 - x *= 3 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 6 }) - }) - - test('divide', () => { - const { frames } = interpret(` - let x = 6 - x /= 3 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 6 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 2 }) - }) - }) - }) - }) - - describe('variable', () => { - test('declare and use', () => { - const { frames } = interpret('let x = 2') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - }) - - test('declare and use', () => { - const { frames } = interpret(` - let x = 2 - let y = x + 1 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 2, y: 3 }) - }) - }) - - describe('function', () => { - describe('without parameters', () => { - test('define', () => { - const { frames } = interpret(` - function move() { - return 1 - } - `) - expect(frames).toBeEmpty() - }) - - describe('call', () => { - test('single statement function', () => { - const { frames } = interpret(` - function move() { - return 1 - } - let x = move() - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 1 }) - }) - }) - }) - - describe('with parameters', () => { - test('define', () => { - const { frames } = interpret(` - function move(x) { - return 1 + x - } - `) - expect(frames).toBeEmpty() - }) - - describe('call', () => { - test('single statement function', () => { - const { frames } = interpret(` - function move(x) { - return 1 + x - } - let x = move(2) - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 3 }) - }) - - describe('default parameter', () => { - test("don't pass in value", () => { - const { frames } = interpret(` - function move(x = 10) { - return 1 + x - } - let x = move() - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 10 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 11 }) - }) - - test('pass in value', () => { - const { frames } = interpret(` - function move(x = 10) { - return 1 + x - } - let x = move(2) - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 3 }) - }) - }) - }) - }) - }) - - describe('if', () => { - test('without else', () => { - const { frames } = interpret(` - if (true) { - let x = 2 - } - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 2 }) - }) - - test('with else', () => { - const { frames } = interpret(` - if (false) { - let x = 2 - } - else { - let x = 3 - } - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 3 }) - }) - - test('nested', () => { - const { frames } = interpret(` - if (false) { - let x = 2 - } - else if (true) { - let x = 3 - } - else { - let x = 4 - } - `) - expect(frames).toBeArrayOfSize(3) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toBeEmpty() - expect(frames[2].status).toBe('SUCCESS') - expect(frames[2].variables).toMatchObject({ x: 3 }) - }) - }) - - describe('while', () => { - test('once', () => { - const { frames } = interpret(` - let x = 1 - while (x > 0) { - x = x - 1 - } - `) - 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(` - let x = 3 - while (x > 0) { - x = x - 1 - } - `) - 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('do/while', () => { - test('once', () => { - const { frames } = interpret(` - let x = 2 - do { - x = x - 1 - } - while (x > 1) - `) - expect(frames).toBeArrayOfSize(3) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 2 }) - 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: 1 }) - }) - - test('multiple times', () => { - const { frames } = interpret(` - let x = 3 - do { - x = x - 1 - } - while (x > 0) - `) - expect(frames).toBeArrayOfSize(7) - 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: 2 }) - 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: 1 }) - 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: 0 }) - expect(frames[6].status).toBe('SUCCESS') - expect(frames[6].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( - ` - for (let num of []) { - echo(num) - } - `, - 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( - ` - for (let num of [1, 2, 3]) { - echo(num) - } - `, - 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(` - { - let x = 1 - let y = 2 - } - `) - 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: 2 }) - }) - - test('nested', () => { - const { frames } = interpret(` - { - let x = 1 - { - let y = 2 - } - } - `) - 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({ y: 2 }) - }) - }) -}) - -describe('frames', () => { - describe('single statement', () => { - test('literal', () => { - const { frames } = interpret('125') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('125') - expect(frames[0].error).toBeNil() - expect(frames[0].variables).toBeEmpty() - }) - - test('call', () => { - const echoFunction = (_interpreter: any, _n: any) => {} - const context = { - externalFunctions: [ - { name: 'echo', func: echoFunction, description: '' }, - ], - } - const { frames } = interpret('echo(1)', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('echo(1)') - expect(frames[0].error).toBeNil() - expect(frames[0].variables).toBeEmpty() - }) - - test('variable', () => { - const { frames } = interpret('let x = 1') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('let x = 1') - expect(frames[0].error).toBeNil() - expect(frames[0].variables).toMatchObject({ x: 1 }) - }) - - test('constant', () => { - const { frames } = interpret('const x = 1') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('const x = 1') - expect(frames[0].error).toBeNil() - expect(frames[0].variables).toMatchObject({ x: 1 }) - }) - }) - - describe('multiple statements', () => { - test('multiple calls', () => { - const context = { - externalFunctions: [ - { - name: 'echo', - func: (_: any, n: any) => {}, - description: '', - }, - ], - } - const { frames } = interpret( - ` - echo(1) - echo(2) - `, - context - ) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].line).toBe(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('echo(1)') - expect(frames[0].error).toBeNil() - expect(frames[1].line).toBe(3) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].code).toBe('echo(2)') - expect(frames[1].error).toBeNil() - }) - }) - - test('no error', () => { - const { frames, error } = interpret('125') - expect(frames).not.toBeEmpty() - expect(error).toBeNull() - }) -}) - -describe('timing', () => { - describe('single statement', () => { - test('success', () => { - const context = { - externalFunctions: [{ name: 'echo', func: () => {}, description: '' }], - } - const { frames } = interpret('echo(1)', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].time).toBe(0) - }) - - test('error', () => { - const context = { - externalFunctions: [{ name: 'echo', func: () => {}, description: '' }], - } - const { frames } = interpret('127()', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].time).toBe(0) - }) - }) - - describe('multiple statements', () => { - test('all successes', () => { - const context = { - externalFunctions: [ - { name: 'echo', func: (_i: any, _n: any) => {}, description: '' }, - ], - } - const { frames } = interpret( - ` - echo(1) - echo(2) - `, - context - ) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].time).toBe(0) - expect(frames[1].time).toBe(1) - }) - }) - - describe('execution context', () => { - test('from non-user code', () => { - const advanceTimeFunction = ( - { fastForward }: ExecutionContext, - n: number - ) => fastForward(n) - const context = { - externalFunctions: [ - { - name: 'advanceTime', - func: advanceTimeFunction, - description: '', - }, - ], - } - const { frames } = interpret( - ` - 1 - advanceTime(20) - 2 - `, - context - ) - expect(frames).toBeArrayOfSize(3) - expect(frames[0].time).toBe(0) - expect(frames[1].time).toBe(1) - expect(frames[2].time).toBe(22) - }) - - test('from user code is not possible', () => { - const { frames } = interpret('fastForward(100)') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].time).toBe(0) - }) - - test('manipulate state', () => { - const state = { count: 10 } - const incrementFunction = ({ state }: ExecutionContext) => { - state.count++ - } - const context = { - externalFunctions: [ - { name: 'increment', func: incrementFunction, description: '' }, - ], - state: state, - } - const { frames } = interpret('increment()', context) - expect(state.count).toBe(11) - - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].time).toBe(0) - }) - }) -}) - -describe('evaluateFunction', () => { - test('without arguments', () => { - const { value, frames } = evaluateFunction( - ` - function move() { - return 1 - } - `, - {}, - 'move' - ) - expect(value).toBe(1) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].result?.value.value).toBe(1) - }) - - test('with arguments', () => { - const { value, frames } = evaluateFunction( - ` - function move(x, y) { - return x + y - } - `, - {}, - 'move', - 1, - 2 - ) - expect(value).toBe(3) - expect(frames).toBeArrayOfSize(1) - }) - - test('with complex arguments', () => { - const { value, frames } = evaluateFunction( - ` - function move(car, speeds) { - return car["x"] + speeds[1] - } - `, - {}, - 'move', - { x: 2 }, - [4, 5, 6] - ) - expect(value).toBe(7) - expect(frames).toBeArrayOfSize(1) - }) - - test('idempotent', () => { - const code = ` - let x = 1 - function move() { - x = x + 1 - return x - }` - const interpreter = new Interpreter(code, {}) - interpreter.compile() - const { value: value1 } = interpreter.evaluateFunction('move') - const { value: value2 } = interpreter.evaluateFunction('move') - expect(value1).toBe(2) - expect(value2).toBe(2) - }) - - test('full program', () => { - const code = ` - function chooseEnemy(enemies) { - let maxRight = 0 - let rightmostEnemyId = null - - for (let enemy of enemies) { - if (enemy["coords"][0] > maxRight) { - maxRight = enemy["coords"][0] - rightmostEnemyId = enemy["id"] - } - } - - return rightmostEnemyId - } - ` - - const poses = [ - { id: 1, coords: [2, 4] }, - { id: 2, coords: [3, 1] }, - ] - const { value, frames } = evaluateFunction(code, {}, 'chooseEnemy', poses) - expect(value).toBe(2) - expect(frames).toBeArrayOfSize(11) - }) - - test('twoFer', () => { - const code = ` - function twoFer(name) { - if(name == "") { - return "One for you, one for me." - } - else { - return "One for " + name + ", one for me." - } - } - ` - - const { value } = evaluateFunction(code, {}, 'twoFer', 'Alice') - expect(value).toEqual('One for Alice, one for me.') - }) -}) - -describe('errors', () => { - test('scanner', () => { - const { frames, error } = interpret('let 123#') - expect(frames).toBeEmpty() - expect(error).not.toBeNull() - expect(error!.category).toBe('SyntaxError') - expect(error!.type).toBe('UnknownCharacter') - expect(error!.context?.character).toBe('#') - }) - - test('parser', () => { - const { frames, error } = interpret('"abc') - expect(frames).toBeEmpty() - expect(error).not.toBeNull() - expect(error!.category).toBe('SyntaxError') - expect(error!.type).toBe('MissingDoubleQuoteToTerminateString') - expect(error!.context).toBeNull - }) - - test('resolver', () => { - const { frames, error } = interpret('return 1') - expect(frames).toBeEmpty() - expect(error).not.toBeNull() - expect(error!.category).toBe('SemanticError') - expect(error!.type).toBe('TopLevelReturn') - expect(error!.context).toBeNull - }) - - describe('runtime', () => { - describe('evaluateFunction', () => { - test('first frame', () => { - const { value, frames, error } = evaluateFunction( - ` - function move() { - foo() - } - `, - {}, - 'move' - ) - expect(value).toBeUndefined() - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(3) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('foo()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('CouldNotFindFunctionWithName') - expect(error).toBeNull() - }) - - test('later frame', () => { - const code = ` - function move() { - let x = 1 - let y = 2 - foo() - } - ` - const { value, frames, error } = evaluateFunction(code, {}, 'move') - - expect(value).toBeUndefined() - expect(frames).toBeArrayOfSize(3) - expect(frames[2].line).toBe(5) - expect(frames[2].status).toBe('ERROR') - expect(frames[2].code).toBe('foo()') - expect(frames[2].error).not.toBeNull() - expect(frames[2].error!.category).toBe('RuntimeError') - expect(frames[2].error!.type).toBe('CouldNotFindFunctionWithName') - expect(error).toBeNull() - }) - }) - }) - - describe('interpret', () => { - describe('call', () => { - test('non-callable', () => { - const { frames, error } = interpret('1()') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('1()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('NonCallableTarget') - expect(error).toBeNull() - }) - - describe('arity', () => { - describe('no optional parameters', () => { - test('too many arguments', () => { - const context = { - externalFunctions: [ - { - name: 'echo', - func: (_: any) => {}, - description: '', - }, - ], - } - const { frames, error } = interpret('echo(1, 2)', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('echo(1, 2)') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('TooManyArguments') - expect(error).toBeNull() - }) - - test('too few arguments', () => { - const context = { - externalFunctions: [ - { - name: 'echo', - func: (_int: any, _: any) => {}, - description: '', - }, - ], - } - const { frames, error } = interpret('echo()', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('echo()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('TooFewArguments') - expect(error).toBeNull() - }) - - // These tests don't make sense. - describe.skip('with optional parameters', () => { - test('too many arguments', () => { - const context = { - externalFunctions: [ - { - name: 'echo', - func: () => {}, - description: '', - }, - ], - } - const { frames, error } = interpret( - ` - function echo(a, b, c, d = 5) { - } - echo(1, 2, 3, 4) - `, - context - ) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('echo(1, 2, 3, 4)') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe( - 'InvalidNumberOfArgumentsWithOptionalArguments' - ) - expect(error).toBeNull() - }) - - test.skip('too few arguments', () => { - const context = { - externalFunctions: { - echo: () => {}, - }, - } - const { frames, error } = interpret('echo(1)', context) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('echo(1)') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe( - 'InvalidNumberOfArgumentsWithOptionalArguments' - ) - expect(error).toBeNull() - }) - }) - }) - }) - - describe('unknown function', () => { - test('not misspelled', () => { - const { frames, error } = interpret('foo()') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(1) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('foo()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('CouldNotFindFunctionWithName') - expect(error).toBeNull() - }) - - test('misspelled', () => { - const { frames, error } = interpret(` - function foobar() { - return 1 - } - - foobor() - `) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(6) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('foobor()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.message).toBe( - "We don't know what `foobor` means. Maybe you meant to use the `foobar` function instead?" - ) - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe( - 'CouldNotFindFunctionWithNameSuggestion' - ) - expect(error).toBeNull() - }) - }) - - test('missing parentheses', () => { - const { frames, error } = interpret(` - function foo() { - return 1 - } - - foo - `) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(6) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('foo') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.message).toBe( - 'Did you forget the parenthesis when trying to call the function?' - ) - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('MissingParenthesesForFunctionCall') - expect(error).toBeNull() - }) - - test('after success', () => { - const { frames, error } = interpret(` - 123 - foo() - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].line).toBe(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].code).toBe('123') - expect(frames[0].error).toBeNil() - expect(frames[1].line).toBe(3) - expect(frames[1].status).toBe('ERROR') - expect(frames[1].code).toBe('foo()') - expect(frames[1].error).not.toBeNull() - expect(frames[1].error!.category).toBe('RuntimeError') - expect(frames[1].error!.type).toBe('CouldNotFindFunctionWithName') - expect(error).toBeNull() - }) - - test('stop execution after error', () => { - const { frames, error } = interpret(` - foo() - 123 - `) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].line).toBe(2) - expect(frames[0].status).toBe('ERROR') - expect(frames[0].code).toBe('foo()') - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.category).toBe('RuntimeError') - expect(frames[0].error!.type).toBe('CouldNotFindFunctionWithName') - expect(error).toBeNull() - }) - }) - }) - - describe('suggestions', () => { - test('function name differs by one letter', () => { - const code = ` - function move() { - m0ve() - } - ` - const { frames, error } = evaluateFunction(code, {}, 'move') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].error).not.toBeNull() - expect(frames[0].error!.context).toMatchObject({ - didYouMean: { - function: 'move', - variable: null, - }, - }) - }) - - test('variable name differs by one letter', () => { - const code = 'let size = 23' - const { frames } = evaluateFunction(code, {}, 'saize + 2') - - expect(frames).toBeArrayOfSize(2) - expect(frames[1].error).not.toBeNull() - expect(frames[1].error!.context).toMatchObject({ - didYouMean: { - function: null, - variable: 'size', - }, - }) - }) - }) -}) - -describe('context', () => { - describe('wrap top-level statements', () => { - // This test doesn't make a huge amount of sense to me. - // as main doesn't actually return - test.skip('wrap non-function statements', () => { - const code = ` - function move(x, y) { - return x + y - } - - let x = 1 - let y = 2 - move(x, y) - ` - const { value, frames } = evaluateFunction( - code, - { wrapTopLevelStatements: true }, - 'main' - ) - expect(value).toBe(3) - expect(frames).toBeArrayOfSize(4) - expect(frames[3].result?.value.value).toBe(3) - }) - - test("don't wrap function declarations", () => { - const { value, frames } = evaluateFunction( - ` - function move() { - return 1 - } - `, - {}, - 'move' - ) - expect(value).toBe(1) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].result?.value.value).toBe(1) - }) - }) -}) diff --git a/test/javascript/interpreter/languages/javascript/parser.test.ts b/test/javascript/interpreter/languages/javascript/parser.test.ts deleted file mode 100644 index a056d1c669..0000000000 --- a/test/javascript/interpreter/languages/javascript/parser.test.ts +++ /dev/null @@ -1,1258 +0,0 @@ -import { - ArrayExpression, - BinaryExpression, - CallExpression, - GroupingExpression, - LiteralExpression, - DictionaryExpression, - VariableExpression, - GetExpression, - SetExpression, - UnaryExpression, - TemplateLiteralExpression, - TemplatePlaceholderExpression, - TemplateTextExpression, - LogicalExpression, - AssignExpression, - UpdateExpression, - TernaryExpression, -} from '@/interpreter/expression' -import { - BlockStatement, - ConstantStatement, - DoWhileStatement, - ExpressionStatement, - FunctionStatement, - IfStatement, - ReturnStatement, - VariableStatement, - WhileStatement, -} from '@/interpreter/statement' -import { parse } from '@/interpreter/languages/javascript/parser' - -describe('literals', () => { - describe('numbers', () => { - test('integer', () => { - const stmts = parse('1') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBe(1) - }) - test('floating points', () => { - const stmts = parse('1.5') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBe(1.5) - }) - }) - - test('string', () => { - const stmts = parse('"nice"') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBe('nice') - }) - - test('true', () => { - const stmts = parse('true') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBe(true) - }) - - test('false', () => { - const stmts = parse('false') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBe(false) - }) - - test('null', () => { - const stmts = parse('null') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LiteralExpression) - const literalExpr = exprStmt.expression as LiteralExpression - expect(literalExpr.value).toBeNull() - }) -}) - -describe('array', () => { - test('empty', () => { - const stmts = parse('[]') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(ArrayExpression) - const arrayExpr = exprStmt.expression as ArrayExpression - expect(arrayExpr.elements).toBeEmpty() - }) - - test('single element', () => { - const stmts = parse('[1]') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(ArrayExpression) - const arrayExpr = exprStmt.expression as ArrayExpression - expect(arrayExpr.elements).toBeArrayOfSize(1) - expect(arrayExpr.elements[0]).toBeInstanceOf(LiteralExpression) - const firstElemExpr = arrayExpr.elements[0] as LiteralExpression - expect(firstElemExpr.value).toBe(1) - }) - - test('multiple elements', () => { - const stmts = parse('[1,2,3]') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(ArrayExpression) - const arrayExpr = exprStmt.expression as ArrayExpression - expect(arrayExpr.elements).toBeArrayOfSize(3) - expect(arrayExpr.elements[0]).toBeInstanceOf(LiteralExpression) - expect(arrayExpr.elements[1]).toBeInstanceOf(LiteralExpression) - expect(arrayExpr.elements[2]).toBeInstanceOf(LiteralExpression) - expect((arrayExpr.elements[0] as LiteralExpression).value).toBe(1) - expect((arrayExpr.elements[1] as LiteralExpression).value).toBe(2) - expect((arrayExpr.elements[2] as LiteralExpression).value).toBe(3) - }) - - test('nested', () => { - const stmts = parse('[1,[2,[3]]]') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(ArrayExpression) - const arrayExpr = exprStmt.expression as ArrayExpression - expect(arrayExpr.elements).toBeArrayOfSize(2) - expect(arrayExpr.elements[0]).toBeInstanceOf(LiteralExpression) - expect((arrayExpr.elements[0] as LiteralExpression).value).toBe(1) - expect(arrayExpr.elements[1]).toBeInstanceOf(ArrayExpression) - const nestedExpr = arrayExpr.elements[1] as ArrayExpression - expect(nestedExpr.elements).toBeArrayOfSize(2) - expect(nestedExpr.elements[0]).toBeInstanceOf(LiteralExpression) - expect((nestedExpr.elements[0] as LiteralExpression).value).toBe(2) - expect(nestedExpr.elements[0]).toBeInstanceOf(LiteralExpression) - const nestedNestedExpr = nestedExpr.elements[1] as ArrayExpression - expect(nestedNestedExpr.elements).toBeArrayOfSize(1) - expect(nestedNestedExpr.elements[0]).toBeInstanceOf(LiteralExpression) - expect((nestedNestedExpr.elements[0] as LiteralExpression).value).toBe(3) - }) - - test('expressions', () => { - const stmts = parse('[-1,2*2,3+3]') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(ArrayExpression) - const arrayExpr = exprStmt.expression as ArrayExpression - expect(arrayExpr.elements).toBeArrayOfSize(3) - expect(arrayExpr.elements[0]).toBeInstanceOf(UnaryExpression) - expect(arrayExpr.elements[1]).toBeInstanceOf(BinaryExpression) - expect(arrayExpr.elements[2]).toBeInstanceOf(BinaryExpression) - }) -}) - -describe('dictionary', () => { - test('empty', () => { - const stmts = parse('let empty = {}') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement - expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) - const mapExpr = varStmt.initializer as DictionaryExpression - expect(mapExpr.elements).toBeEmpty() - }) - - test('single element', () => { - const stmts = parse('let movie = {"title": "Jurassic Park"}') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement - expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) - const mapExpr = varStmt.initializer as DictionaryExpression - expect(mapExpr.elements.size).toBe(1) - expect(mapExpr.elements.get('title')).toBeInstanceOf(LiteralExpression) - expect((mapExpr.elements.get('title') as LiteralExpression).value).toBe( - 'Jurassic Park' - ) - }) - - test('multiple elements', () => { - const stmts = parse('let movie = {"title": "Jurassic Park", "year": 1993}') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement - expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) - const mapExpr = varStmt.initializer as DictionaryExpression - expect(mapExpr.elements.size).toBe(2) - expect(mapExpr.elements.get('title')).toBeInstanceOf(LiteralExpression) - expect((mapExpr.elements.get('title') as LiteralExpression).value).toBe( - 'Jurassic Park' - ) - expect(mapExpr.elements.get('year')).toBeInstanceOf(LiteralExpression) - expect((mapExpr.elements.get('year') as LiteralExpression).value).toBe(1993) - }) - - test('nested', () => { - const stmts = parse( - 'let movie = {"title": "Jurassic Park", "director": { "name": "Steven Spielberg" } }' - ) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement - expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) - const mapExpr = varStmt.initializer as DictionaryExpression - expect(mapExpr.elements.size).toBe(2) - expect(mapExpr.elements.get('title')).toBeInstanceOf(LiteralExpression) - expect((mapExpr.elements.get('title') as LiteralExpression).value).toBe( - 'Jurassic Park' - ) - expect(mapExpr.elements.get('director')).toBeInstanceOf( - DictionaryExpression - ) - const nestedMapExpr = mapExpr.elements.get( - 'director' - ) as DictionaryExpression - expect(nestedMapExpr.elements.size).toBe(1) - expect(nestedMapExpr.elements.get('name')).toBeInstanceOf(LiteralExpression) - expect( - (nestedMapExpr.elements.get('name') as LiteralExpression).value - ).toBe('Steven Spielberg') - }) -}) - -describe('variable', () => { - test('single-character name', () => { - const statements = parse('let x = 1') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const varStatement = statements[0] as VariableStatement - expect(varStatement.name.lexeme).toBe('x') - const literalExpr = varStatement.initializer as LiteralExpression - expect(literalExpr.value).toBe(1) - }) - - test('multi-character name', () => { - const statements = parse('let fooBar = "abc"') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const varStatement = statements[0] as VariableStatement - expect(varStatement.name.lexeme).toBe('fooBar') - const literalExpr = varStatement.initializer as LiteralExpression - expect(literalExpr.value).toBe('abc') - }) -}) - -describe('assignment', () => { - test('regular', () => { - const statements = parse(` - let x = 1 - x = 2 - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(AssignExpression) - const assignExpr = exprStatement.expression as AssignExpression - expect(assignExpr.name.lexeme).toBe('x') - expect(assignExpr.operator.type).toBe('EQUAL') - expect(assignExpr.value).toBeInstanceOf(LiteralExpression) - const literalExpr = assignExpr.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - - describe('compound', () => { - test('plus', () => { - const statements = parse(` - let x = 1 - x += 2 - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(AssignExpression) - const assignExpr = exprStatement.expression as AssignExpression - expect(assignExpr.name.lexeme).toBe('x') - expect(assignExpr.operator.type).toBe('PLUS_EQUAL') - expect(assignExpr.value).toBeInstanceOf(LiteralExpression) - const literalExpr = assignExpr.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - - test('minus', () => { - const statements = parse(` - let x = 1 - x -= 2 - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(AssignExpression) - const assignExpr = exprStatement.expression as AssignExpression - expect(assignExpr.name.lexeme).toBe('x') - expect(assignExpr.operator.type).toBe('MINUS_EQUAL') - expect(assignExpr.value).toBeInstanceOf(LiteralExpression) - const literalExpr = assignExpr.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - - test('multiply', () => { - const statements = parse(` - let x = 1 - x *= 2 - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(AssignExpression) - const assignExpr = exprStatement.expression as AssignExpression - expect(assignExpr.name.lexeme).toBe('x') - expect(assignExpr.operator.type).toBe('STAR_EQUAL') - expect(assignExpr.value).toBeInstanceOf(LiteralExpression) - const literalExpr = assignExpr.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - - test('divide', () => { - const statements = parse(` - let x = 1 - x /= 2 - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(AssignExpression) - const assignExpr = exprStatement.expression as AssignExpression - expect(assignExpr.name.lexeme).toBe('x') - expect(assignExpr.operator.type).toBe('SLASH_EQUAL') - expect(assignExpr.value).toBeInstanceOf(LiteralExpression) - const literalExpr = assignExpr.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - }) -}) - -describe('increment', () => { - test('variable', () => { - const statements = parse(` - let x = 1 - x++ - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(UpdateExpression) - const incrementExpr = exprStatement.expression as UpdateExpression - expect(incrementExpr.operator.type).toBe('PLUS_PLUS') - expect(incrementExpr.operand).toBeInstanceOf(VariableExpression) - const variableExpr = incrementExpr.operand as VariableExpression - expect(variableExpr.name.lexeme).toBe('x') - }) - - test('array', () => { - const statements = parse(` - let x = [1,2] - x[0]++ - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(UpdateExpression) - const incrementExpr = exprStatement.expression as UpdateExpression - expect(incrementExpr.operator.type).toBe('PLUS_PLUS') - expect(incrementExpr.operand).toBeInstanceOf(GetExpression) - const getExpr = incrementExpr.operand as GetExpression - expect(getExpr.field.lexeme).toBe('0') - expect(getExpr.obj).toBeInstanceOf(VariableExpression) - const variableExpr = getExpr.obj as VariableExpression - expect(variableExpr.name.lexeme).toBe('x') - }) - - test('dictionary', () => { - const statements = parse(` - let x = {"count": 1} - x["count"]++ - `) - expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(ExpressionStatement) - const exprStatement = statements[1] as ExpressionStatement - expect(exprStatement.expression).toBeInstanceOf(UpdateExpression) - const incrementExpr = exprStatement.expression as UpdateExpression - expect(incrementExpr.operator.type).toBe('PLUS_PLUS') - expect(incrementExpr.operand).toBeInstanceOf(GetExpression) - const getExpr = incrementExpr.operand as GetExpression - expect(getExpr.field.lexeme).toBe('"count"') - expect(getExpr.obj).toBeInstanceOf(VariableExpression) - const variableExpr = getExpr.obj as VariableExpression - expect(variableExpr.name.lexeme).toBe('x') - }) -}) - -describe('call', () => { - test('without arguments', () => { - const stmts = parse('move()') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const expStmt = stmts[0] as ExpressionStatement - expect(expStmt.expression).toBeInstanceOf(CallExpression) - const callExpr = expStmt.expression as CallExpression - expect(callExpr.args).toBeEmpty() - }) - - test('single argument', () => { - const stmts = parse('turn("left")') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(CallExpression) - const callExpr = exprStmt.expression as CallExpression - expect(callExpr.args).toBeArrayOfSize(1) - expect(callExpr.args[0]).toBeInstanceOf(LiteralExpression) - }) - - test('chained', () => { - const stmts = parse('turn("left")("right")') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(CallExpression) - const callExpr = exprStmt.expression as CallExpression - expect(callExpr.args).toBeArrayOfSize(1) - expect(callExpr.args[0]).toBeInstanceOf(LiteralExpression) - expect(callExpr.callee).toBeInstanceOf(CallExpression) - const nestedCallExpr = callExpr.callee as CallExpression - expect(nestedCallExpr.args).toBeArrayOfSize(1) - expect(nestedCallExpr.args[0]).toBeInstanceOf(LiteralExpression) - }) -}) - -describe('get', () => { - describe('dictionary', () => { - test('single field', () => { - const stmts = parse(` - let movie = {"title": "The Matrix"} - let title = movie["title"] - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement - expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) - const getExpr = varStmtWithGet.initializer as GetExpression - expect(getExpr.field.literal).toBe('title') - expect(getExpr.obj).toBeInstanceOf(VariableExpression) - expect((getExpr.obj as VariableExpression).name.lexeme).toBe('movie') - }) - - test('chained', () => { - const stmts = parse(` - let movie = {"director": {"name": "Peter Jackson"}} - let director = movie["director"]["name"] - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement - expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) - const getExpr = varStmtWithGet.initializer as GetExpression - expect(getExpr.field.literal).toBe('name') - expect(getExpr.obj).toBeInstanceOf(GetExpression) - const nestedGetExpr = getExpr.obj as GetExpression - expect(nestedGetExpr.field.literal).toBe('director') - expect(nestedGetExpr.obj).toBeInstanceOf(VariableExpression) - expect((nestedGetExpr.obj as VariableExpression).name.lexeme).toBe( - 'movie' - ) - }) - }) - - describe('array', () => { - test('single field', () => { - const stmts = parse(` - let scores = [7, 3, 10] - let latest = scores[2] - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement - expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) - const getExpr = varStmtWithGet.initializer as GetExpression - expect(getExpr.field.literal).toBe(2) - expect(getExpr.obj).toBeInstanceOf(VariableExpression) - expect((getExpr.obj as VariableExpression).name.lexeme).toBe('scores') - }) - - test('chained', () => { - const stmts = parse(` - let scoreMinMax = [[3, 7], [1, 6]] - let secondMin = scoreMinMax[1][0] - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement - expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) - const getExpr = varStmtWithGet.initializer as GetExpression - expect(getExpr.field.literal).toBe(0) - expect(getExpr.obj).toBeInstanceOf(GetExpression) - const nestedGetExpr = getExpr.obj as GetExpression - expect(nestedGetExpr.field.literal).toBe(1) - expect(nestedGetExpr.obj).toBeInstanceOf(VariableExpression) - expect((nestedGetExpr.obj as VariableExpression).name.lexeme).toBe( - 'scoreMinMax' - ) - }) - }) -}) - -describe('set', () => { - describe('dictionary', () => { - test('single field', () => { - const stmts = parse(` - let movie = {"title": "The Matrix"} - movie["title"] = "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(` - let movie = {"director": {"name": "Peter Jackson"}} - movie["director"]["name"] = "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)') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(GroupingExpression) - const groupingExpr = exprStmt.expression as GroupingExpression - expect(groupingExpr.inner).toBeInstanceOf(BinaryExpression) - const binaryExpr = groupingExpr.inner as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('PLUS') - }) - - test('nested', () => { - const stmts = parse('(1 + (2 - 3))') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(GroupingExpression) - const groupingExpr = exprStmt.expression as GroupingExpression - expect(groupingExpr.inner).toBeInstanceOf(BinaryExpression) - const binaryExpr = groupingExpr.inner as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(GroupingExpression) - expect(binaryExpr.operator.type).toBe('PLUS') - const nestedGroupingExpr = binaryExpr.right as GroupingExpression - expect(nestedGroupingExpr.inner).toBeInstanceOf(BinaryExpression) - const nestedBinaryExpr = nestedGroupingExpr.inner as BinaryExpression - expect(nestedBinaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(nestedBinaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(nestedBinaryExpr.operator.type).toBe('MINUS') - }) -}) - -describe('binary', () => { - test('addition', () => { - const stmts = parse('1 + 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('PLUS') - }) - - test('subtraction', () => { - const stmts = parse('1 - 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('MINUS') - }) - - test('multiplication', () => { - const stmts = parse('1 * 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('STAR') - }) - - test('division', () => { - const stmts = parse('1 / 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('SLASH') - }) - - test('string concatenation', () => { - const stmts = parse('"hello" + "world"') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.left).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - expect(binaryExpr.operator.type).toBe('PLUS') - }) - - describe('nesting', () => { - test('numbers', () => { - const stmts = parse('1 + 2 * 3 / 4 - 5') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.operator.type).toBe('MINUS') - expect(binaryExpr.left).toBeInstanceOf(BinaryExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - - const binaryExprRight = binaryExpr.right as LiteralExpression - expect(binaryExprRight.value).toBe(5) - - const binaryExprLeft = binaryExpr.left as BinaryExpression - expect(binaryExprLeft.operator.type).toBe('PLUS') - expect(binaryExprLeft.left).toBeInstanceOf(LiteralExpression) - expect(binaryExprLeft.right).toBeInstanceOf(BinaryExpression) - - const binaryExprLeftLeft = binaryExprLeft.left as LiteralExpression - expect(binaryExprLeftLeft.value).toBe(1) - - const binaryExprLeftRight = binaryExprLeft.right as BinaryExpression - expect(binaryExprLeftRight.operator.type).toBe('SLASH') - expect(binaryExprLeftRight.left).toBeInstanceOf(BinaryExpression) - expect(binaryExprLeftRight.right).toBeInstanceOf(LiteralExpression) - - const binaryExprLeftRightRight = - binaryExprLeftRight.right as LiteralExpression - expect(binaryExprLeftRightRight.value).toBe(4) - - const binaryExprLeftRightLeft = - binaryExprLeftRight.left as BinaryExpression - expect(binaryExprLeftRightLeft.operator.type).toBe('STAR') - expect(binaryExprLeftRightLeft.left).toBeInstanceOf(LiteralExpression) - expect(binaryExprLeftRightLeft.right).toBeInstanceOf(LiteralExpression) - const binaryExprLeftRightLeftLeft = - binaryExprLeftRightLeft.left as LiteralExpression - expect(binaryExprLeftRightLeftLeft.value).toBe(2) - const binaryExprLeftRightLeftRight = - binaryExprLeftRightLeft.right as LiteralExpression - expect(binaryExprLeftRightLeftRight.value).toBe(3) - }) - - test('string concatenation', () => { - const stmts = parse('"hello" + "world" + "!"') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(BinaryExpression) - const binaryExpr = exprStmt.expression as BinaryExpression - expect(binaryExpr.operator.type).toBe('PLUS') - expect(binaryExpr.left).toBeInstanceOf(BinaryExpression) - expect(binaryExpr.right).toBeInstanceOf(LiteralExpression) - - const binaryExprRight = binaryExpr.right as LiteralExpression - expect(binaryExprRight.value).toBe('!') - - const binaryExprLeft = binaryExpr.left as BinaryExpression - expect(binaryExprLeft.left).toBeInstanceOf(LiteralExpression) - expect(binaryExprLeft.right).toBeInstanceOf(LiteralExpression) - - const binaryExprLeftLeft = binaryExprLeft.left as LiteralExpression - expect(binaryExprLeftLeft.value).toBe('hello') - - const binaryExprLeftRight = binaryExprLeft.right as LiteralExpression - expect(binaryExprLeftRight.value).toBe('world') - }) - }) -}) - -describe('logical', () => { - test('and', () => { - const stmts = parse('true and false') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LogicalExpression) - const logicalExpr = exprStmt.expression as LogicalExpression - expect(logicalExpr.left).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.right).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.operator.type).toBe('AND') - }) - - test('&&', () => { - const stmts = parse('true && false') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LogicalExpression) - const logicalExpr = exprStmt.expression as LogicalExpression - expect(logicalExpr.left).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.right).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.operator.type).toBe('AND') - }) - - test('or', () => { - const stmts = parse('true or false') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LogicalExpression) - const logicalExpr = exprStmt.expression as LogicalExpression - expect(logicalExpr.left).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.right).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.operator.type).toBe('OR') - }) - - test('||', () => { - const stmts = parse('true || false') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(LogicalExpression) - const logicalExpr = exprStmt.expression as LogicalExpression - expect(logicalExpr.left).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.right).toBeInstanceOf(LiteralExpression) - expect(logicalExpr.operator.type).toBe('OR') - }) -}) - -describe('ternary', () => { - test('non-nested', () => { - const stmts = parse('true ? 1 : 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const expStmt = stmts[0] as ExpressionStatement - expect(expStmt.expression).toBeInstanceOf(TernaryExpression) - const ternaryExpr = expStmt.expression as TernaryExpression - expect(ternaryExpr.thenBranch).toBeInstanceOf(LiteralExpression) - expect(ternaryExpr.elseBranch).toBeInstanceOf(LiteralExpression) - }) - - test('nested', () => { - const stmts = parse('true ? 1 : false ? 2 : 3') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const expStmt = stmts[0] as ExpressionStatement - expect(expStmt.expression).toBeInstanceOf(TernaryExpression) - const ternaryExpr = expStmt.expression as TernaryExpression - expect(ternaryExpr.thenBranch).toBeInstanceOf(LiteralExpression) - expect(ternaryExpr.elseBranch).toBeInstanceOf(TernaryExpression) - const nestedTernaryExpr = ternaryExpr.elseBranch as TernaryExpression - expect(nestedTernaryExpr.thenBranch).toBeInstanceOf(LiteralExpression) - expect(nestedTernaryExpr.elseBranch).toBeInstanceOf(LiteralExpression) - }) -}) - -describe('if', () => { - test('without else', () => { - const stmts = parse(` - if (true) { - let x = 1 - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(IfStatement) - const expStmt = stmts[0] as IfStatement - expect(expStmt.condition).toBeInstanceOf(LiteralExpression) - expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) - const thenStmt = expStmt.thenBranch as BlockStatement - expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) - expect(expStmt.elseBranch).toBeNil() - }) - - test('with else', () => { - const stmts = parse(` - if (true) { - let x = 1 - } - else { - let x = 2 - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(IfStatement) - const expStmt = stmts[0] as IfStatement - expect(expStmt.condition).toBeInstanceOf(LiteralExpression) - expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) - const thenStmt = expStmt.thenBranch as BlockStatement - expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) - expect(expStmt.elseBranch).toBeInstanceOf(BlockStatement) - const elseStmt = expStmt.elseBranch as BlockStatement - expect(elseStmt.statements).toBeArrayOfSize(1) - expect(elseStmt.statements[0]).toBeInstanceOf(VariableStatement) - }) - - test('nested', () => { - const stmts = parse(` - if (true) { - let x = 1 - } - else if (false) { - let x = 2 - } - else { - let x = 3 - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(IfStatement) - const expStmt = stmts[0] as IfStatement - expect(expStmt.condition).toBeInstanceOf(LiteralExpression) - expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) - const thenStmt = expStmt.thenBranch as BlockStatement - expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) - expect(expStmt.elseBranch).toBeInstanceOf(IfStatement) - const elseIfStmt = expStmt.elseBranch as IfStatement - expect(elseIfStmt.condition).toBeInstanceOf(LiteralExpression) - expect(elseIfStmt.thenBranch).toBeInstanceOf(BlockStatement) - const elseIfStmtThenBlock = elseIfStmt.thenBranch as BlockStatement - expect(elseIfStmtThenBlock.statements).toBeArrayOfSize(1) - expect(elseIfStmtThenBlock.statements[0]).toBeInstanceOf(VariableStatement) - expect(elseIfStmt.elseBranch).toBeInstanceOf(BlockStatement) - const elseIfStmtElseBlock = elseIfStmt.elseBranch as BlockStatement - expect(elseIfStmtElseBlock.statements).toBeArrayOfSize(1) - expect(elseIfStmtElseBlock.statements[0]).toBeInstanceOf(VariableStatement) - }) -}) - -describe('while', () => { - test('with single statement', () => { - const stmts = parse(` - while (true) { - let x = 1 - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(WhileStatement) - const expStmt = stmts[0] as WhileStatement - expect(expStmt.condition).toBeInstanceOf(LiteralExpression) - expect(expStmt.body).toBeArrayOfSize(1) - expect(expStmt.body[0]).toBeInstanceOf(VariableStatement) - }) -}) - -describe('do while', () => { - test('with single statement', () => { - const stmts = parse(` - do { - let x = 1 - } - while (true) - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(DoWhileStatement) - const expStmt = stmts[0] as DoWhileStatement - expect(expStmt.condition).toBeInstanceOf(LiteralExpression) - expect(expStmt.body).toBeArrayOfSize(1) - expect(expStmt.body[0]).toBeInstanceOf(VariableStatement) - }) -}) - -describe('block', () => { - test('non-nested', () => { - const stmts = parse(` - { - let x = 1 - let y = 2 - } - `) - expect(stmts).toBeArrayOfSize(1) - 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) - }) - - test('nested', () => { - const stmts = parse(` - { - let x = 1 - { - let y = 2 - } - } - `) - expect(stmts).toBeArrayOfSize(1) - 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(BlockStatement) - }) -}) - -describe('function', () => { - test('without parameters', () => { - const stmts = parse(` - function move() { - return 1 - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(FunctionStatement) - const functionStmt = stmts[0] as FunctionStatement - expect(functionStmt.name.lexeme).toBe('move') - expect(functionStmt.parameters).toBeEmpty() - expect(functionStmt.body).toBeArrayOfSize(1) - expect(functionStmt.body[0]).toBeInstanceOf(ReturnStatement) - }) - - test('with parameters', () => { - const stmts = parse(` - function move(from, to) { - return from + to - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(FunctionStatement) - const functionStmt = stmts[0] as FunctionStatement - expect(functionStmt.name.lexeme).toBe('move') - expect(functionStmt.parameters).toBeArrayOfSize(2) - expect(functionStmt.parameters[0].name.lexeme).toBe('from') - expect(functionStmt.parameters[1].name.lexeme).toBe('to') - expect(functionStmt.body).toBeArrayOfSize(1) - expect(functionStmt.body[0]).toBeInstanceOf(ReturnStatement) - }) - - test('with default parameter', () => { - const stmts = parse(` - function move(from = 0, to = 10) { - return from + to - } - `) - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(FunctionStatement) - const functionStmt = stmts[0] as FunctionStatement - expect(functionStmt.name.lexeme).toBe('move') - expect(functionStmt.parameters).toBeArrayOfSize(2) - expect(functionStmt.parameters[0].name.lexeme).toBe('from') - expect(functionStmt.parameters[1].name.lexeme).toBe('to') - expect(functionStmt.body).toBeArrayOfSize(1) - expect(functionStmt.body[0]).toBeInstanceOf(ReturnStatement) - }) -}) - -describe('return', () => { - test('without argument', () => { - const stmts = parse('return') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ReturnStatement) - const returnStmt = stmts[0] as ReturnStatement - expect(returnStmt.value).toBeNull() - }) - - describe('with argument', () => { - test('number', () => { - const stmts = parse('return 2') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ReturnStatement) - const returnStmt = stmts[0] as ReturnStatement - expect(returnStmt.value).toBeInstanceOf(LiteralExpression) - const literalExpr = returnStmt.value as LiteralExpression - expect(literalExpr.value).toBe(2) - }) - - test('string', () => { - const stmts = parse('return "hello there!"') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ReturnStatement) - const returnStmt = stmts[0] as ReturnStatement - expect(returnStmt.value).toBeInstanceOf(LiteralExpression) - const literalExpr = returnStmt.value as LiteralExpression - expect(literalExpr.value).toBe('hello there!') - }) - }) -}) - -describe('error', () => { - describe('number', () => { - test('two periods', () => { - expect(() => parse('1.3.4')).toThrow( - 'A number can only have one decimal point. Did you mean `1.34`?' - ) - }) - }) - describe('string', () => { - test('unstarted', () => { - expect(() => parse('abc"')).toThrow( - 'Did you forget the start quote for the "abc" string?' - ) - }) - test('unterminated - end of file', () => { - expect(() => parse('"abc')).toThrow( - `Did you forget to add end quote? Maybe you meant to write:\n\n\`\`\`\"abc\"\`\`\`` - ) - }) - test('unterminated - end of line', () => { - expect(() => parse('"abc\nsomething_else"')).toThrow( - `Did you forget to add end quote? Maybe you meant to write:\n\n\`\`\`\"abc\"\`\`\`` - ) - }) - test('unterminated - newline in string', () => { - expect(() => parse('"abc\n"')).toThrow( - `Did you forget to add end quote? Maybe you meant to write:\n\n\`\`\`\"abc\"\`\`\`` - ) - }) - }) - - describe('call', () => { - test('missing opening parenthesis', () => { - expect(() => - parse(` - function move() { - return 1 - } - - move) - `) - ).toThrow( - 'Did you forget the start parenthesis when trying to call the move function?' - ) - }) - - test('missing closing parenthesis', () => { - expect(() => parse('move(1')).toThrow( - 'Did you forget the end parenthesis when trying to call the move function?' - ) - }) - }) - - describe('statement', () => { - test('multiple expressions on single line', () => { - expect(() => parse('1 1')).toThrow( - "We didn't expect `{{current}}` to appear on this line after the `{{previous}}`. {{suggestion}}" - ) - }) - }) -}) - -describe('white space', () => { - test('skip over empty lines', () => { - const stmts = parse(` - let a = 19 - - \t\t\t - let x = true - - let y = false - \t - - `) - expect(stmts).toBeArrayOfSize(3) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - expect(stmts[2]).toBeInstanceOf(VariableStatement) - }) -}) - -describe('location', () => { - describe('statement', () => { - test('expression', () => { - const statements = parse('123') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(ExpressionStatement) - const expressionStatement = statements[0] as ExpressionStatement - expect(expressionStatement.location.line).toBe(1) - expect(expressionStatement.location.relative.begin).toBe(1) - expect(expressionStatement.location.relative.end).toBe(4) - expect(expressionStatement.location.absolute.begin).toBe(1) - expect(expressionStatement.location.absolute.end).toBe(4) - }) - - test('variable', () => { - const statements = parse('let x = 1') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const expressionStatement = statements[0] as VariableStatement - expect(expressionStatement.location.line).toBe(1) - expect(expressionStatement.location.relative.begin).toBe(1) - expect(expressionStatement.location.relative.end).toBe(10) - expect(expressionStatement.location.absolute.begin).toBe(1) - expect(expressionStatement.location.absolute.end).toBe(10) - }) - - test('const', () => { - const statements = parse('const x = 1') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(ConstantStatement) - const expressionStatement = statements[0] as ConstantStatement - expect(expressionStatement.location.line).toBe(1) - expect(expressionStatement.location.relative.begin).toBe(1) - expect(expressionStatement.location.relative.end).toBe(12) - expect(expressionStatement.location.absolute.begin).toBe(1) - expect(expressionStatement.location.absolute.end).toBe(12) - }) - }) - - describe('expression', () => { - test('literal', () => { - const statements = parse('123') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(ExpressionStatement) - const expressionStatement = statements[0] as ExpressionStatement - const expression = expressionStatement.expression - expect(expression).toBeInstanceOf(LiteralExpression) - - expect(expression.location.line).toBe(1) - expect(expression.location.relative.begin).toBe(1) - expect(expression.location.relative.end).toBe(4) - expect(expression.location.absolute.begin).toBe(1) - expect(expression.location.absolute.end).toBe(4) - }) - - test('variable', () => { - const statements = parse('foo') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(ExpressionStatement) - const expressionStatement = statements[0] as ExpressionStatement - const expression = expressionStatement.expression - expect(expression).toBeInstanceOf(VariableExpression) - expect(expression.location.line).toBe(1) - expect(expression.location.relative.begin).toBe(1) - expect(expression.location.relative.end).toBe(4) - expect(expression.location.absolute.begin).toBe(1) - expect(expression.location.absolute.end).toBe(4) - }) - - test('call', () => { - const statements = parse('move(7)') - expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(ExpressionStatement) - const expressionStatement = statements[0] as ExpressionStatement - const expression = expressionStatement.expression - expect(expression).toBeInstanceOf(CallExpression) - expect(expression.location.line).toBe(1) - expect(expression.location.relative.begin).toBe(1) - expect(expression.location.relative.end).toBe(8) - expect(expression.location.absolute.begin).toBe(1) - expect(expression.location.absolute.end).toBe(8) - }) - }) -}) diff --git a/test/javascript/interpreter/languages/javascript/scanner.test.ts b/test/javascript/interpreter/languages/javascript/scanner.test.ts deleted file mode 100644 index b617dd66e0..0000000000 --- a/test/javascript/interpreter/languages/javascript/scanner.test.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { scan } from '@/interpreter/languages/javascript/scanner' -import type { TokenType } from '@/interpreter/languages/javascript/token' - -describe('single-character', () => { - test.each([ - ['{', 'LEFT_BRACE'], - ['[', 'LEFT_BRACKET'], - ['(', 'LEFT_PAREN'], - ['}', 'RIGHT_BRACE'], - [']', 'RIGHT_BRACKET'], - [')', 'RIGHT_PAREN'], - [':', 'COLON'], - [',', 'COMMA'], - ['-', 'MINUS'], - ['+', 'PLUS'], - ['*', 'STAR'], - ['/', 'SLASH'], - ['?', 'QUESTION_MARK'], - ])("'%s' token", (source: string, expectedType: string) => { - const tokens = scan(source) - expect(tokens[0].type).toBe(expectedType as TokenType) - expect(tokens[0].lexeme).toBe(source) - expect(tokens[0].literal).toBeNull - }) -}) - -describe('one, two or three characters', () => { - test.each([ - ['=', 'EQUAL'], - - ['==', 'EQUALITY'], - ['!=', 'INEQUALITY'], - ['!==', 'STRICT_INEQUALITY'], - ['===', 'STRICT_EQUALITY'], - - ['!', 'NOT'], - ['>', 'GREATER'], - ['>=', 'GREATER_EQUAL'], - ['<', 'LESS'], - ['<=', 'LESS_EQUAL'], - ['&&', 'AMPERSAND_AMPERSAND'], - ['||', 'PIPE_PIPE'], - ['*=', 'STAR_EQUAL'], - ['/=', 'SLASH_EQUAL'], - ['+=', 'PLUS_EQUAL'], - ['++', 'PLUS_PLUS'], - ['-=', 'MINUS_EQUAL'], - ['--', 'MINUS_MINUS'], - ])("'%s' token", (source: string, expectedType: string) => { - const tokens = scan(source) - expect(tokens[0].type).toBe(expectedType as TokenType) - expect(tokens[0].lexeme).toBe(source) - expect(tokens[0].literal).toBeNull - }) -}) - -describe('keyword', () => { - test.each([ - ['and', 'AND'], - ['const', 'CONST'], - ['do', 'DO'], - ['else', 'ELSE'], - ['false', 'FALSE'], - ['for', 'FOR'], - ['function', 'FUNCTION'], - ['if', 'IF'], - ['in', 'IN'], - ['let', 'LET'], - ['null', 'NULL'], - ['of', 'OF'], - ['or', 'OR'], - ['return', 'RETURN'], - ['true', 'TRUE'], - ['while', 'WHILE'], - ])("'%s' keyword", (source: string, expectedType: string) => { - const tokens = scan(source) - expect(tokens[0].type).toBe(expectedType as TokenType) - expect(tokens[0].lexeme).toBe(source) - expect(tokens[0].literal).toBeNull - }) -}) - -describe('string', () => { - test('empty', () => { - const tokens = scan('""') - expect(tokens[0].type).toBe('STRING') - expect(tokens[0].lexeme).toBe('""') - expect(tokens[0].literal).toBe('') - }) - - test('single character', () => { - const tokens = scan('"a"') - expect(tokens[0].type).toBe('STRING') - expect(tokens[0].lexeme).toBe('"a"') - expect(tokens[0].literal).toBe('a') - }) - - test('multiple characters', () => { - const tokens = scan('"Hello"') - expect(tokens[0].type).toBe('STRING') - expect(tokens[0].lexeme).toBe('"Hello"') - expect(tokens[0].literal).toBe('Hello') - }) - - test('containing whitespace', () => { - const tokens = scan('" Good\tday! "') - expect(tokens[0].type).toBe('STRING') - expect(tokens[0].lexeme).toBe('" Good\tday! "') - expect(tokens[0].literal).toBe(' Good\tday! ') - }) - - test('containing number', () => { - const tokens = scan('"Testing 1,2,3"') - expect(tokens[0].type).toBe('STRING') - expect(tokens[0].lexeme).toBe('"Testing 1,2,3"') - expect(tokens[0].literal).toBe('Testing 1,2,3') - }) -}) - -describe('template literal', () => { - test('empty', () => { - const tokens = scan('``') - expect(tokens.length).toBeGreaterThanOrEqual(2) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('BACKTICK') - expect(tokens[1].lexeme).toBe('`') - expect(tokens[1].literal).toBeNull() - }) - - describe('text only', () => { - test('single character', () => { - const tokens = scan('`a`') - expect(tokens.length).toBeGreaterThanOrEqual(3) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[1].lexeme).toBe('a') - expect(tokens[1].literal).toBe('a') - expect(tokens[2].type).toBe('BACKTICK') - expect(tokens[2].lexeme).toBe('`') - expect(tokens[2].literal).toBeNull() - }) - - test('multiple characters', () => { - const tokens = scan('`hello`') - expect(tokens.length).toBeGreaterThanOrEqual(3) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[1].lexeme).toBe('hello') - expect(tokens[1].literal).toBe('hello') - expect(tokens[2].type).toBe('BACKTICK') - expect(tokens[2].lexeme).toBe('`') - expect(tokens[2].literal).toBeNull() - }) - - test('containing whitespace', () => { - const tokens = scan('` Good\tday! `') - expect(tokens.length).toBeGreaterThanOrEqual(3) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[1].lexeme).toBe(' Good\tday! ') - expect(tokens[1].literal).toBe(' Good\tday! ') - expect(tokens[2].type).toBe('BACKTICK') - expect(tokens[2].lexeme).toBe('`') - expect(tokens[2].literal).toBeNull() - }) - - test('containing number', () => { - const tokens = scan('`Testing 1,2,3`') - expect(tokens.length).toBeGreaterThanOrEqual(3) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[1].lexeme).toBe('Testing 1,2,3') - expect(tokens[1].literal).toBe('Testing 1,2,3') - expect(tokens[2].type).toBe('BACKTICK') - expect(tokens[2].lexeme).toBe('`') - expect(tokens[2].literal).toBeNull() - }) - }) - - describe('placeholder only', () => { - test('string', () => { - const tokens = scan('`${"hello"}`') - expect(tokens.length).toBeGreaterThanOrEqual(5) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[1].lexeme).toBe('${') - expect(tokens[1].literal).toBeNull() - expect(tokens[2].type).toBe('STRING') - expect(tokens[2].lexeme).toBe('"hello"') - expect(tokens[2].literal).toBe('hello') - expect(tokens[3].type).toBe('RIGHT_BRACE') - expect(tokens[3].lexeme).toBe('}') - expect(tokens[3].literal).toBeNull() - expect(tokens[4].type).toBe('BACKTICK') - expect(tokens[4].lexeme).toBe('`') - expect(tokens[4].literal).toBeNull() - }) - - test('binary expression', () => { - const tokens = scan('`${2*4}`') - expect(tokens.length).toBeGreaterThanOrEqual(7) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[1].lexeme).toBe('${') - expect(tokens[1].literal).toBeNull() - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[2].lexeme).toBe('2') - expect(tokens[2].literal).toBe(2) - expect(tokens[3].type).toBe('STAR') - expect(tokens[3].lexeme).toBe('*') - expect(tokens[3].literal).toBeNull() - expect(tokens[4].type).toBe('NUMBER') - expect(tokens[4].lexeme).toBe('4') - expect(tokens[4].literal).toBe(4) - expect(tokens[5].type).toBe('RIGHT_BRACE') - expect(tokens[5].lexeme).toBe('}') - expect(tokens[5].literal).toBeNull() - expect(tokens[6].type).toBe('BACKTICK') - expect(tokens[6].lexeme).toBe('`') - expect(tokens[6].literal).toBeNull() - }) - }) - - test('text and placeholders', () => { - const tokens = scan('`sum of ${2} * ${3} is ${"six"}`') - expect(tokens.length).toBeGreaterThanOrEqual(5) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[1].lexeme).toBe('sum of ') - expect(tokens[1].literal).toBe('sum of ') - expect(tokens[2].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[2].lexeme).toBe('${') - expect(tokens[2].literal).toBeNull() - expect(tokens[3].type).toBe('NUMBER') - expect(tokens[3].lexeme).toBe('2') - expect(tokens[3].literal).toBe(2) - expect(tokens[4].type).toBe('RIGHT_BRACE') - expect(tokens[4].lexeme).toBe('}') - expect(tokens[4].literal).toBeNull() - expect(tokens[5].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[5].lexeme).toBe(' * ') - expect(tokens[5].literal).toBe(' * ') - expect(tokens[6].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[6].lexeme).toBe('${') - expect(tokens[6].literal).toBeNull() - expect(tokens[7].type).toBe('NUMBER') - expect(tokens[7].lexeme).toBe('3') - expect(tokens[7].literal).toBe(3) - expect(tokens[8].type).toBe('RIGHT_BRACE') - expect(tokens[8].lexeme).toBe('}') - expect(tokens[8].literal).toBeNull() - expect(tokens[9].type).toBe('TEMPLATE_LITERAL_TEXT') - expect(tokens[9].lexeme).toBe(' is ') - expect(tokens[9].literal).toBe(' is ') - expect(tokens[10].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[10].lexeme).toBe('${') - expect(tokens[10].literal).toBeNull() - expect(tokens[11].type).toBe('STRING') - expect(tokens[11].lexeme).toBe('"six"') - expect(tokens[11].literal).toBe('six') - expect(tokens[12].type).toBe('RIGHT_BRACE') - expect(tokens[12].lexeme).toBe('}') - expect(tokens[12].literal).toBeNull() - }) - - test('binary expression', () => { - const tokens = scan('`${2*4}`') - expect(tokens.length).toBeGreaterThanOrEqual(7) - expect(tokens[0].type).toBe('BACKTICK') - expect(tokens[0].lexeme).toBe('`') - expect(tokens[0].literal).toBeNull() - expect(tokens[1].type).toBe('DOLLAR_LEFT_BRACE') - expect(tokens[1].lexeme).toBe('${') - expect(tokens[1].literal).toBeNull() - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[2].lexeme).toBe('2') - expect(tokens[2].literal).toBe(2) - expect(tokens[3].type).toBe('STAR') - expect(tokens[3].lexeme).toBe('*') - expect(tokens[3].literal).toBeNull() - expect(tokens[4].type).toBe('NUMBER') - expect(tokens[4].lexeme).toBe('4') - expect(tokens[4].literal).toBe(4) - expect(tokens[5].type).toBe('RIGHT_BRACE') - expect(tokens[5].lexeme).toBe('}') - expect(tokens[5].literal).toBeNull() - expect(tokens[6].type).toBe('BACKTICK') - expect(tokens[6].lexeme).toBe('`') - expect(tokens[6].literal).toBeNull() - }) -}) - -describe('identifier', () => { - test('start with lower letter', () => { - const tokens = scan('name') - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[0].lexeme).toBe('name') - expect(tokens[0].literal).toBeNull - }) - - test('start with upper letter', () => { - const tokens = scan('Name') - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[0].lexeme).toBe('Name') - expect(tokens[0].literal).toBeNull - }) - - test('start with underscore', () => { - const tokens = scan('_name') - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[0].lexeme).toBe('_name') - expect(tokens[0].literal).toBeNull - }) -}) - -describe('number', () => { - test('integer', () => { - const tokens = scan('143') - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[0].lexeme).toBe('143') - expect(tokens[0].literal).toBe(143) - }) - - test('floating-point', () => { - const tokens = scan('76.9') - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[0].lexeme).toBe('76.9') - expect(tokens[0].literal).toBe(76.9) - }) -}) - -describe('call', () => { - test('without arguments', () => { - const tokens = scan('move()') - expect(tokens).toBeArrayOfSize(5) - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[1].type).toBe('LEFT_PAREN') - expect(tokens[2].type).toBe('RIGHT_PAREN') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('EOF') - }) - - test('single string argument', () => { - const tokens = scan('turn("left")') - expect(tokens).toBeArrayOfSize(6) - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[1].type).toBe('LEFT_PAREN') - expect(tokens[2].type).toBe('STRING') - expect(tokens[3].type).toBe('RIGHT_PAREN') - expect(tokens[4].type).toBe('EOL') - expect(tokens[5].type).toBe('EOF') - }) - - test('single integer argument', () => { - const tokens = scan('advance(5)') - expect(tokens).toBeArrayOfSize(6) - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[1].type).toBe('LEFT_PAREN') - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[3].type).toBe('RIGHT_PAREN') - expect(tokens[4].type).toBe('EOL') - expect(tokens[5].type).toBe('EOF') - }) -}) - -test('multiple lines', () => { - const tokens = scan('move()\nturn("left")\nmove()') - expect(tokens).toBeArrayOfSize(14) - expect(tokens[0].type).toBe('IDENTIFIER') - expect(tokens[1].type).toBe('LEFT_PAREN') - expect(tokens[2].type).toBe('RIGHT_PAREN') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('IDENTIFIER') - expect(tokens[5].type).toBe('LEFT_PAREN') - expect(tokens[6].type).toBe('STRING') - expect(tokens[7].type).toBe('RIGHT_PAREN') - expect(tokens[8].type).toBe('EOL') - expect(tokens[9].type).toBe('IDENTIFIER') - expect(tokens[10].type).toBe('LEFT_PAREN') - expect(tokens[11].type).toBe('RIGHT_PAREN') - expect(tokens[12].type).toBe('EOL') - expect(tokens[13].type).toBe('EOF') -}) - -test('location', () => { - const tokens = scan('move()\nturn("left")') - expect(tokens).toBeArrayOfSize(10) - - expect(tokens[0].location.line).toBe(1) - expect(tokens[0].location.relative.begin).toBe(1) - expect(tokens[0].location.relative.end).toBe(5) - expect(tokens[0].location.absolute.begin).toBe(1) - expect(tokens[0].location.absolute.end).toBe(5) - - expect(tokens[1].location.line).toBe(1) - expect(tokens[1].location.relative.begin).toBe(5) - expect(tokens[1].location.relative.end).toBe(6) - expect(tokens[1].location.absolute.begin).toBe(5) - expect(tokens[1].location.absolute.end).toBe(6) - - expect(tokens[2].location.line).toBe(1) - expect(tokens[2].location.relative.begin).toBe(6) - expect(tokens[2].location.relative.end).toBe(7) - expect(tokens[2].location.absolute.begin).toBe(6) - expect(tokens[2].location.absolute.end).toBe(7) - - expect(tokens[3].location.line).toBe(1) - expect(tokens[3].location.relative.begin).toBe(7) - expect(tokens[3].location.relative.end).toBe(8) - expect(tokens[3].location.absolute.begin).toBe(7) - expect(tokens[3].location.absolute.end).toBe(8) - - expect(tokens[4].location.line).toBe(2) - expect(tokens[4].location.relative.begin).toBe(1) - expect(tokens[4].location.relative.end).toBe(5) - expect(tokens[4].location.absolute.begin).toBe(8) - expect(tokens[4].location.absolute.end).toBe(12) - - expect(tokens[5].location.line).toBe(2) - expect(tokens[5].location.relative.begin).toBe(5) - expect(tokens[5].location.relative.end).toBe(6) - expect(tokens[5].location.absolute.begin).toBe(12) - expect(tokens[5].location.absolute.end).toBe(13) - - expect(tokens[6].location.line).toBe(2) - expect(tokens[6].location.relative.begin).toBe(6) - expect(tokens[6].location.relative.end).toBe(12) - expect(tokens[6].location.absolute.begin).toBe(13) - expect(tokens[6].location.absolute.end).toBe(19) - - expect(tokens[7].location.line).toBe(2) - expect(tokens[7].location.relative.begin).toBe(12) - expect(tokens[7].location.relative.end).toBe(13) - expect(tokens[7].location.absolute.begin).toBe(19) - expect(tokens[7].location.absolute.end).toBe(20) -}) - -describe('error', () => { - describe('token', () => { - test('invalid', () => { - expect(() => scan('123#')).toThrow("Unknown character: '#'.") - }) - - test('Exclude listed', () => { - expect(() => scan('const x = 1', { excludeList: ['CONST'] })).toThrow( - "Jiki doesn't know how to use `const` in this exercise." - ) - }) - - test('Include listed', () => { - expect(() => - scan('const x = 1', { includeList: ['IDENTIFIER', 'NUMBER'] }) - ).toThrow("Jiki doesn't know how to use `const` in this exercise.") - }) - }) -}) - -describe('white space', () => { - describe('ignore', () => { - test('spaces', () => { - const tokens = scan(' ') - expect(tokens).toHaveLength(1) - expect(tokens[0].type).toBe('EOF') - }) - - test('tabs', () => { - const tokens = scan('\t\t\t') - expect(tokens).toHaveLength(1) - expect(tokens[0].type).toBe('EOF') - }) - - test('consecutive newlines', () => { - const tokens = scan('\n\n\n\n') - expect(tokens).toHaveLength(1) - expect(tokens[0].type).toBe('EOF') - }) - - test('between statements', () => { - const tokens = scan('1\n\n2\n') - expect(tokens).toHaveLength(5) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('EOF') - }) - }) -}) - -describe('synthetic', () => { - describe('EOL', () => { - describe('not added', () => { - test('empty line', () => { - const tokens = scan('') - expect(tokens).toBeArrayOfSize(1) - expect(tokens[0].type).toBe('EOF') - }) - - test('before first statement', () => { - const tokens = scan('\n1\n') - expect(tokens).toHaveLength(3) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('EOF') - }) - - test('between statements', () => { - const tokens = scan('1\n\n2\n') - expect(tokens).toHaveLength(5) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('EOF') - }) - }) - - describe('added', () => { - describe('single statement', () => { - test('ending with newline', () => { - const tokens = scan('1\n') - expect(tokens).toBeArrayOfSize(3) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('EOF') - }) - - test('not ending with newline', () => { - const tokens = scan('1') - expect(tokens).toBeArrayOfSize(3) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('EOF') - }) - }) - - describe('multiple statements', () => { - test('ending with newline', () => { - const tokens = scan('1\n2\n') - expect(tokens).toBeArrayOfSize(5) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('EOF') - }) - - test('last statement not ending with newline', () => { - const tokens = scan('1\n2') - expect(tokens).toBeArrayOfSize(5) - expect(tokens[0].type).toBe('NUMBER') - expect(tokens[1].type).toBe('EOL') - expect(tokens[2].type).toBe('NUMBER') - expect(tokens[3].type).toBe('EOL') - expect(tokens[4].type).toBe('EOF') - }) - }) - }) - }) -}) diff --git a/test/javascript/interpreter/localization.test.ts b/test/javascript/interpreter/localization.test.ts index d27bc977da..be84e3be38 100644 --- a/test/javascript/interpreter/localization.test.ts +++ b/test/javascript/interpreter/localization.test.ts @@ -1,4 +1,4 @@ -import { scan } from '@/interpreter/languages/javascript/scanner' +import { scan } from '@/interpreter/scanner' import { changeLanguage, getLanguage } from '@/interpreter/translator' async function usingLanguage(newLanguage: string, callback: () => void) { diff --git a/test/javascript/interpreter/languages/jikiscript/parser.test.ts b/test/javascript/interpreter/parser.test.ts similarity index 99% rename from test/javascript/interpreter/languages/jikiscript/parser.test.ts rename to test/javascript/interpreter/parser.test.ts index a7f75de430..87954d52a2 100644 --- a/test/javascript/interpreter/languages/jikiscript/parser.test.ts +++ b/test/javascript/interpreter/parser.test.ts @@ -25,7 +25,7 @@ import { VariableStatement, WhileStatement, } from '@/interpreter/statement' -import { parse } from '@/interpreter/languages/jikiscript/parser' +import { parse } from '@/interpreter/parser' describe('comments', () => { test('basic text', () => { diff --git a/test/javascript/interpreter/resolver.test.ts b/test/javascript/interpreter/resolver.test.ts deleted file mode 100644 index beb2cc1a1d..0000000000 --- a/test/javascript/interpreter/resolver.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { type Context } from '@/interpreter/interpreter' -import { parse } from '@/interpreter/languages/javascript/parser' -import { Resolver } from '@/interpreter/resolver' - -export function resolve(sourceCode: string, context: Context = {}): void { - const externalFunctions = context.externalFunctions || {} - const functionNames = Object.keys(externalFunctions) - const statements = parse(sourceCode, { - functionNames: functionNames, - languageFeatures: context.languageFeatures || {}, - shouldWrapTopLevelStatements: context.wrapTopLevelStatements || false, - }) - new Resolver(false, functionNames).resolve(statements) -} - -describe('error', () => { - test('declare variable with same name more than once', () => { - expect(() => - resolve(` - { - let a = 1 - let a = 2 - } - `) - ).toThrow('Already a variable with this name in this scope.') - }) - - describe('assign constant', () => { - test('in same scope', () => { - expect(() => - resolve(` - const a = 1 - a = 2 - `) - ).toThrow('Cannot re-assign value of constant.') - }) - - test('in parent scope', () => { - expect(() => - resolve(` - const a = 1 - { - a = 2 - } - `) - ).toThrow('Cannot re-assign value of constant.') - }) - }) - - test('return outside of function', () => { - expect(() => resolve('return 1')).toThrow( - "Can't return from top-level code." - ) - }) -}) diff --git a/test/javascript/interpreter/languages/jikiscript/scanner.test.ts b/test/javascript/interpreter/scanner.test.ts similarity index 99% rename from test/javascript/interpreter/languages/jikiscript/scanner.test.ts rename to test/javascript/interpreter/scanner.test.ts index e4a7b06c2e..0756cab87b 100644 --- a/test/javascript/interpreter/languages/jikiscript/scanner.test.ts +++ b/test/javascript/interpreter/scanner.test.ts @@ -1,5 +1,5 @@ -import { scan } from '@/interpreter/languages/jikiscript/scanner' -import { type TokenType } from '@/interpreter/languages/jikiscript/token' +import { scan } from '@/interpreter/scanner' +import { type TokenType } from '@/interpreter/token' describe('single-character', () => { test.each([ diff --git a/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts b/test/javascript/interpreter/syntaxErrors.test.ts similarity index 99% rename from test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts rename to test/javascript/interpreter/syntaxErrors.test.ts index 85160c339b..7f5a58174e 100644 --- a/test/javascript/interpreter/languages/jikiscript/syntaxErrors.test.ts +++ b/test/javascript/interpreter/syntaxErrors.test.ts @@ -1,4 +1,4 @@ -import { parse } from '@/interpreter/languages/jikiscript/parser' +import { parse } from '@/interpreter/parser' import { changeLanguage } from '@/interpreter/translator' beforeAll(() => { diff --git a/test/javascript/validate_bootcamp_content.test.js b/test/javascript/validate_bootcamp_content.test.js deleted file mode 100644 index d8fed13d50..0000000000 --- a/test/javascript/validate_bootcamp_content.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import { evaluateJikiScriptFunction } from '@/interpreter/interpreter' -import fs from 'fs' -import path from 'path' - -const contentDir = path.resolve(__dirname, '../../bootcamp_content/projects') - -function getSubdirectories(dir) { - return fs - .readdirSync(dir) - .filter((file) => fs.statSync(path.join(dir, file)).isDirectory()) -} - -function getConfig(exerciseDir) { - const configPath = path.join(exerciseDir, 'config.json') - return JSON.parse(fs.readFileSync(configPath, 'utf-8')) -} - -function getExampleScript(exerciseDir) { - const examplePath = path.join(exerciseDir, 'example.jiki') - return fs.readFileSync(examplePath, 'utf-8') -} - -describe('Exercise Tests', () => { - const projects = getSubdirectories(contentDir) - - projects.forEach((project) => { - const projectDir = contentDir + '/' + project + '/exercises' - const exercises = getSubdirectories(projectDir) - exercises.forEach((exercise) => { - const exerciseDir = path.join(projectDir, exercise) - const config = getConfig(exerciseDir) - const exampleScript = getExampleScript(exerciseDir) - - if (config.tests_type == 'state') { - return - } - - config.tasks.forEach((task) => { - task.tests.forEach((taskTest) => { - test(`${project} - ${exercise} - ${task.name} - ${taskTest.name}`, () => { - let error, value, frames - try { - ;({ error, value, frames } = evaluateJikiScriptFunction( - exampleScript, - {}, - taskTest.function, - ...taskTest.params - )) - } catch (e) { - console.log(e) - expect(true).toBe(false) - } - - // if(value != taskTest.expected.return) { - // console.log(error, value, frames); - // } - expect(value).toEqual(taskTest.expected) - }) - }) - }) - }) - }) -}) diff --git a/test/javascript/validate_bootcamp_content.test.ts b/test/javascript/validate_bootcamp_content.test.ts new file mode 100644 index 0000000000..1aefc38651 --- /dev/null +++ b/test/javascript/validate_bootcamp_content.test.ts @@ -0,0 +1,144 @@ +import { evaluateFunction, interpret } from '@/interpreter/interpreter' +import fs from 'fs' +import path from 'path' +import exerciseMap, { + type Project, +} from '@/components/bootcamp/SolveExercisePage/utils/exerciseMap' +import { Exercise } from '@/components/bootcamp/SolveExercisePage/exercises/Exercise' +import { camelize, camelizeKeys } from 'humps' + +const contentDir = path.resolve(__dirname, '../../bootcamp_content/projects') + +function getSubdirectories(dir) { + return fs + .readdirSync(dir) + .filter((file) => fs.statSync(path.join(dir, file)).isDirectory()) +} + +function getConfig(exerciseDir) { + const configPath = path.join(exerciseDir, 'config.json') + return JSON.parse(fs.readFileSync(configPath, 'utf-8')) +} + +function getExampleScript(exerciseDir) { + const examplePath = path.join(exerciseDir, 'example.jiki') + return fs.readFileSync(examplePath, 'utf-8') +} + +function testIo(project, exercise, task, testData, exampleScript) { + test(`${project} - ${exercise} - ${task.name} - ${testData.name}`, () => { + let error, value, frames + try { + ;({ error, value, frames } = evaluateFunction( + exampleScript, + {}, + testData.function, + ...testData.params + )) + } catch (e) { + expect(true).toBe(false) + } + + // if(value != testData.expected.return) { + // console.log(error, value, frames); + // } + expect(value).toEqual(testData.expected) + }) +} + +function testState( + project, + exerciseSlug, + config, + task, + testData, + exampleScript +) { + test(`${project} - ${exerciseSlug} - ${task.name}`, () => { + const Project = exerciseMap.get(config.projectType) + const exercise: Exercise = new Project() + + ;(testData.setupFunctions || []).forEach((functionData) => { + let [functionName, params] = functionData + if (!params) { + params = [] + } + exercise[functionName](...params) + }) + + const context = { + externalFunctions: exercise.availableFunctions, + languageFeatures: config.interpreterOptions, + } + let evaluated + if (testData.function) { + evaluated = evaluateFunction(exampleScript, context, testData.function) + } else { + evaluated = interpret(exampleScript, context) + } + + const state = exercise.getState() + + testData.checks.forEach((check) => { + const matcher = check.matcher || 'toEqual' + + // check can either be a reference to the final state or a function call. + // We pivot on that to determine the actual value + let actual + + // If it's a function call, we split out any params and then call the function + // on the exercise with those params passed in. + if (check.name.includes('(') && check.name.endsWith(')')) { + const fnName = check.name.slice(0, check.name.indexOf('(')) + const argsString = check.name.slice(check.name.indexOf('(') + 1, -1) + + // We eval the args to turn numbers into numbers, strings into strings, etc. + const safe_eval = eval // https://esbuild.github.io/content-types/#direct-eval + const args = + argsString === '' + ? [] + : argsString.split(',').map((arg) => safe_eval(arg.trim())) + + // And then we get the function and call it. + const fn = exercise[fnName] + actual = fn.bind(exercise).call(exercise, evaluated, ...args) + } + + // Our normal state is much easier! We just check the state object that + // we've retrieved above via getState() for the variable in question. + else { + actual = state[check.name] + } + + expect(actual)[matcher](check.value) + }) + }) +} + +describe('Exercise Tests', () => { + const projects = getSubdirectories(contentDir) + + projects.forEach((project) => { + const projectDir = contentDir + '/' + project + '/exercises' + const exercises = getSubdirectories(projectDir) + exercises.forEach((exercise) => { + const exerciseDir = path.join(projectDir, exercise) + const config = camelizeKeys(getConfig(exerciseDir)) + const exampleScript = getExampleScript(exerciseDir) + + if (config.testsType == 'io') { + config.tasks.forEach((task) => { + task.tests.forEach((testData) => { + testIo(project, exercise, task, testData, exampleScript) + }) + }) + } else { + config.tasks.forEach((task) => { + task.tests.forEach((testData) => { + testState(project, exercise, config, task, testData, exampleScript) + }) + }) + } + }) + }) +})