diff --git a/app/javascript/interpreter/error.ts b/app/javascript/interpreter/error.ts
index 44598400d6..c43723dd4a 100644
--- a/app/javascript/interpreter/error.ts
+++ b/app/javascript/interpreter/error.ts
@@ -108,6 +108,9 @@ export type RuntimeErrorType =
| 'UnexpectedEqualsForEquality'
| 'VariableAlreadyDeclared'
| 'VariableNotDeclared'
+ | 'UnexpectedUncalledFunction'
+ | 'FunctionAlreadyDeclared'
+ | 'UnexpectedChangeOfFunction'
export type StaticErrorType =
| DisabledLanguageFeatureErrorType
diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts
index ae5e95065e..685e0f79aa 100644
--- a/app/javascript/interpreter/executor.ts
+++ b/app/javascript/interpreter/executor.ts
@@ -20,6 +20,7 @@ import {
UnaryExpression,
UpdateExpression,
VariableExpression,
+ ExpressionWithValue,
} from './expression'
import { Location, Span } from './location'
import {
@@ -215,7 +216,12 @@ export class Executor {
public visitSetVariableStatement(statement: SetVariableStatement): void {
this.executeFrame(statement, () => {
- if (this.environment.inScope(statement.name.lexeme)) {
+ if (this.environment.inScope(statement.name)) {
+ if (isCallable(this.environment.get(statement.name))) {
+ this.error('FunctionAlreadyDeclared', statement.name.location, {
+ name: statement.name.lexeme,
+ })
+ }
this.error('VariableAlreadyDeclared', statement.location, {
name: statement.name.lexeme,
})
@@ -252,6 +258,12 @@ export class Executor {
})
}
+ if (isCallable(this.environment.get(statement.name))) {
+ this.error('UnexpectedChangeOfFunction', statement.name.location, {
+ name: statement.name.lexeme,
+ })
+ }
+
const value = this.evaluate(statement.value)
this.updateVariable(statement.name, value.value, statement)
@@ -621,55 +633,85 @@ export class Executor {
}
public visitBinaryExpression(expression: BinaryExpression): EvaluationResult {
- const left = this.evaluate(expression.left)
- const right = this.evaluate(expression.right)
+ const leftResult = this.evaluate(expression.left)
+ const rightResult = this.evaluate(expression.right)
const result: EvaluationResult = {
type: 'BinaryExpression',
value: undefined,
operator: expression.operator.type,
- left,
- right,
+ left: leftResult,
+ right: rightResult,
}
switch (expression.operator.type) {
case 'INEQUALITY':
// TODO: throw error when types are not the same?
- return { ...result, value: left.value !== right.value }
+ return { ...result, value: leftResult.value !== rightResult.value }
case 'EQUALITY':
// TODO: throw error when types are not the same?
return {
...result,
- value: left.value === right.value,
+ value: leftResult.value === rightResult.value,
}
case 'GREATER':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
return {
...result,
- value: left.value > right.value,
+ value: leftResult.value > rightResult.value,
}
case 'GREATER_EQUAL':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
return {
...result,
- value: left.value >= right.value,
+ value: leftResult.value >= rightResult.value,
}
case 'LESS':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
return {
...result,
- value: left.value < right.value,
+ value: leftResult.value < rightResult.value,
}
case 'LESS_EQUAL':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
return {
...result,
- value: left.value <= right.value,
+ value: leftResult.value <= rightResult.value,
}
case 'MINUS':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
- const minusValue = left.value - right.value
+ const minusValue = leftResult.value - rightResult.value
const minusValue2DP = Math.round(minusValue * 100) / 100
return {
@@ -678,8 +720,24 @@ export class Executor {
}
//> binary-plus
case 'PLUS':
- if (isNumber(left.value) && isNumber(right.value)) {
- const plusValue = left.value + right.value
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
+
+ const plusValue = leftResult.value + rightResult.value
+ const plusValue2DP = Math.round(plusValue * 100) / 100
+
+ return {
+ ...result,
+ value: plusValue2DP,
+ }
+
+ if (isNumber(leftResult.value) && isNumber(rightResult.value)) {
+ const plusValue = leftResult.value + rightResult.value
const plusValue2DP = Math.round(plusValue * 100) / 100
return {
@@ -687,7 +745,7 @@ export class Executor {
value: plusValue2DP,
}
}
- if (isString(left.value) && isString(right.value))
+ /*if (isString(left.value) && isString(right.value))
return {
...result,
value: left.value + right.value,
@@ -700,31 +758,49 @@ export class Executor {
left,
right,
}
- )
+ )*/
case 'SLASH':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
- const slashValue = left.value / right.value
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
+ const slashValue = leftResult.value / rightResult.value
const slashValue2DP = Math.round(slashValue * 100) / 100
return {
...result,
value: slashValue2DP,
}
case 'STAR':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
- const starValue = left.value * right.value
+ const starValue = leftResult.value * rightResult.value
const starValue2DP = Math.round(starValue * 100) / 100
return {
...result,
value: starValue2DP,
}
case 'PERCENT':
- this.verifyNumberOperands(expression.operator, left.value, right.value)
+ this.verifyNumberOperands(
+ expression.operator,
+ expression.left,
+ expression.right,
+ leftResult.value,
+ rightResult.value
+ )
return {
...result,
- value: left.value % right.value,
+ value: leftResult.value % rightResult.value,
}
case 'EQUAL':
this.error('UnexpectedEqualsForEquality', expression.location, {
@@ -919,10 +995,48 @@ export class Executor {
this.error('OperandMustBeNumber', operator.location, { operand })
}
- private verifyNumberOperands(operator: Token, left: any, right: any): void {
- if (isNumber(left) && isNumber(right)) return
+ private verifyNumberOperands(
+ operator: Token,
+ leftExpr: Expression,
+ rightExpr: Expression,
+ leftValue: EvaluationResult,
+ rightValue: EvaluationResult
+ ): void {
+ const leftIsNumber = isNumber(leftValue)
+ const rightIsNumber = isNumber(rightValue)
+ if (leftIsNumber && rightIsNumber) {
+ return
+ }
- this.error('OperandsMustBeNumbers', operator.location, { left, right })
+ let value
+ let eroneousExpr
+ let location
+ if (leftIsNumber) {
+ value = rightValue
+ eroneousExpr = rightExpr
+ location = Location.between(operator, rightExpr)
+ } else {
+ value = leftValue
+ eroneousExpr = leftExpr
+ location = Location.between(leftExpr, operator)
+ }
+
+ if (isCallable(value)) {
+ this.error('UnexpectedUncalledFunction', eroneousExpr.location, {
+ name: eroneousExpr.name,
+ })
+ }
+
+ // Quote strings
+ if (typeof value == 'string') {
+ //value = `"${value}""`
+ }
+
+ this.error('OperandsMustBeNumbers', location, {
+ operator: operator.lexeme,
+ side: leftIsNumber ? 'right' : 'left',
+ value: `\`${value}\``,
+ })
}
private verifyBooleanOperand(operand: any, location: Location): void {
diff --git a/app/javascript/interpreter/locales/en/translation.json b/app/javascript/interpreter/locales/en/translation.json
index d231ff5e28..195d334118 100644
--- a/app/javascript/interpreter/locales/en/translation.json
+++ b/app/javascript/interpreter/locales/en/translation.json
@@ -82,13 +82,16 @@
"NonCallableTarget": "Can only call functions.",
"OperandMustBeBoolean": "Operand must be a boolean.",
"OperandMustBeNumber": "Operand must be a number.",
- "OperandsMustBeNumber": "Operands must be numbers.",
+ "OperandsMustBeNumbers": "Both sides of the `{{operator}}` must be numbers. The {{side}} side is not a number. It is {{value}}.",
"OperandsMustBeTwoNumbersOrTwoStrings": "Operands must be two numbers or two strings.",
"RepeatCountMustBeGreaterThanZero": "You must repeat things at least once.",
"RepeatCountMustBeLessThanOneThousand": "The most times you can repeat things is 1000.",
"RepeatCountMustBeNumber": "repeat can only take a number as its argument.",
"VariableAlreadyDeclared": "A variable with this name has already been created. Did you mean to use the change
keyword?",
- "VariableNotDeclared": "Did you forget to use the `set` keyword to create a variable called `{{name}}` before this line of code?"
+ "VariableNotDeclared": "Did you forget to use the `set` keyword to create a variable called `{{name}}` before this line of code?",
+ "UnexpectedUncalledFunction": "You used a function as if it's a variable. Did you mean to use it by adding `()` at the end?",
+ "FunctionAlreadyDeclared": "There is already a function called `{{name}}`. Please choose a different name for this variable.",
+ "UnexpectedChangeOfFunction": "You are trying to change `{{name}}` but it is a function, not a variable.\n\nIt is not a box that you can change the contents of. It is machine for you to use."
},
"disabledLanguageFeature": {
"ExcludeListViolation": "Jiki doesn't know how to use `{{lexeme}}` in this exercise.",
diff --git a/app/javascript/interpreter/locales/system/translation.json b/app/javascript/interpreter/locales/system/translation.json
index bcfd56862f..591258a2b1 100644
--- a/app/javascript/interpreter/locales/system/translation.json
+++ b/app/javascript/interpreter/locales/system/translation.json
@@ -81,12 +81,15 @@
"NonCallableTarget": "NonCallableTarget",
"OperandMustBeBoolean": "OperandMustBeBoolean",
"OperandMustBeNumber": "OperandMustBeNumber",
- "OperandsMustBeNumber": "OperandsMustBeNumber",
+ "OperandsMustBeNumbers": "OperandsMustBeNumbers: operator: {{operator}}, side: {{side}}, value: {{value}}",
"OperandsMustBeTwoNumbersOrTwoStrings": "OperandsMustBeTwoNumbersOrTwoStrings",
"RepeatCountMustBeGreaterThanZero": "RepeatCountMustBeGreaterThanZero",
"RepeatCountMustBeNumber": "RepeatCountMustBeNumber",
"TooManyArguments": "TooManyArguments: arity: {{arity}}, numberOfArgs: {{numberOfArgs}}",
- "TooFewArguments": "TooFewArguments: arity: {{arity}}, numberOfArgs: {{numberOfArgs}}"
+ "TooFewArguments": "TooFewArguments: arity: {{arity}}, numberOfArgs: {{numberOfArgs}}",
+ "UnexpectedUncalledFunction": "UnexpectedUncalledFunction",
+ "FunctionAlreadyDeclared": "FunctionAlreadyDeclared: name: {{name}}",
+ "UnexpectedChangeOfFunction": "UnexpectedChangeOfFunction: name: {{name}}"
},
"disabledLanguageFeature": {
"ExcludeListViolation": "ExcludeListViolation: tokenType: {{tokenType}}",
diff --git a/bootcamp_content/projects/two-fer/config.json b/bootcamp_content/projects/two-fer/config.json
deleted file mode 100644
index 0e383e2435..0000000000
--- a/bootcamp_content/projects/two-fer/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "slug": "two-fer",
- "title": "TwoFer",
- "description": "Cookies, strings, conditionals",
- "exercises": ["basic"]
-}
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/config.json b/bootcamp_content/projects/two-fer/exercises/basic/config.json
deleted file mode 100644
index aed6b3a61c..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/config.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "title": "Empty Strings",
- "description": "Return what you say when you hand the cookie other, using the person's name if you know it.",
- "concepts": ["conditionals"],
- "level": 3,
- "idx": 1,
- "tests_type": "io",
- "readonly_ranges": [
- { "from": 1, "to": 1 },
- { "from": 3, "to": 3 }
- ],
- "tasks": [
- {
- "name": "Get things working with a single string",
- "tests": [
- {
- "slug": "no_name",
- "name": "no name given",
- "function": "two_fer",
- "params": [""],
- "expected": "One for you, one for me."
- }
- ]
- },
- {
- "name": "Get things working with a name",
- "tests": [
- {
- "type": "io",
- "slug": "alice",
- "name": "A name is given",
- "function": "two_fer",
- "params": ["Alice"],
- "expected": "One for Alice, one for me."
- },
- {
- "type": "io",
- "slug": "bob",
- "name": "Another name is given",
- "function": "two_fer",
- "params": ["Bob"],
- "expected": "One for Bob, one for me."
- }
- ]
- }
- ]
-}
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/example.jiki b/bootcamp_content/projects/two-fer/exercises/basic/example.jiki
deleted file mode 100644
index 802eb8f278..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/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/two-fer/exercises/basic/introduction.md b/bootcamp_content/projects/two-fer/exercises/basic/introduction.md
deleted file mode 100644
index c8048e8dea..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/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 , 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/two-fer/exercises/basic/stub.jiki b/bootcamp_content/projects/two-fer/exercises/basic/stub.jiki
deleted file mode 100644
index e871b02348..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/stub.jiki
+++ /dev/null
@@ -1,3 +0,0 @@
-function two_fer with name do
-
-end
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/task-1.md b/bootcamp_content/projects/two-fer/exercises/basic/task-1.md
deleted file mode 100644
index 1b03934503..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/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/two-fer/exercises/basic/task-2.md b/bootcamp_content/projects/two-fer/exercises/basic/task-2.md
deleted file mode 100644
index 2a597efd6a..0000000000
--- a/bootcamp_content/projects/two-fer/exercises/basic/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/two-fer/introduction.md b/bootcamp_content/projects/two-fer/introduction.md
deleted file mode 100644
index f3cf6682e4..0000000000
--- a/bootcamp_content/projects/two-fer/introduction.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Two Fer
-
-## Overview
-
-Two Fer is a classic Exercism exercise.
-Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_.
-
-## Introduction
-
-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, if you know their name (e.g. they're called "Jeremy"), you say:
-
-```text
-"One for Jeremy, one for me."
-```
-
-If you don't know their name (even more generous of you!), you say:
-"One for you, one for me."
-
-```
-
-```
diff --git a/bootcamp_content/projects/wordle/config.json b/bootcamp_content/projects/wordle/config.json
deleted file mode 100644
index 1bbcca61f0..0000000000
--- a/bootcamp_content/projects/wordle/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "slug": "wordle",
- "title": "Wordle",
- "description": "Create the modern Wordle game",
- "exercises": ["process-guess"]
-}
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/config.json b/bootcamp_content/projects/wordle/exercises/process-guess/config.json
deleted file mode 100644
index b33fe3138d..0000000000
--- a/bootcamp_content/projects/wordle/exercises/process-guess/config.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "title": "Process a guess",
- "description": "Turn a guess into a valid response",
- "project_type": "wordle",
- "available_functions": [],
- "concepts": ["conditionals", "arrays"],
- "level": 3,
- "idx": 1,
- "tests_type": "state",
- "tasks": [
- {
- "name": "Deal with a correct guess",
- "tests": [
- {
- "slug": "all-correct",
- "name": "Deal with a fully correct guess",
- "function": "process_guess",
- "checks": [
- {
- "name": "getGuessState1()",
- "value": ["present", "absent", "absent", "absent", "correct"],
- "error_html": "We expected the first letter to be present and the last one to be correct"
- }
- ]
- }
- ]
- }
- ]
-}
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/example.jiki b/bootcamp_content/projects/wordle/exercises/process-guess/example.jiki
deleted file mode 100644
index 7e4fc86905..0000000000
--- a/bootcamp_content/projects/wordle/exercises/process-guess/example.jiki
+++ /dev/null
@@ -1,6 +0,0 @@
-move()
-move()
-turn_left()
-move()
-move()
-turn_right()
\ No newline at end of file
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md b/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md
deleted file mode 100644
index bf45496945..0000000000
--- a/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Solve the Maze
-
-Your task is to solve the following maze.
-
-You have three functions you can use:
-
-- `move()` which moves the character one step forward
-- `turn_left()` turns the character left (relative to the direction they're currently facing)
-- `turn_right()` turns the character right (relative to the direction they're currently facing)
-
-Remember to use one function per line.
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/stub.jk b/bootcamp_content/projects/wordle/exercises/process-guess/stub.jk
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md b/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md
deleted file mode 100644
index b470c01201..0000000000
--- a/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Task 1
-
-We've started by adding a single `move()` for you, which will move the character one step forward.
-
-Use the "Run code" button to see how close you're getting.
-
-It's good practice to get into the habit of running your code reguarly.
-In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/wordle/introduction.md b/bootcamp_content/projects/wordle/introduction.md
deleted file mode 100644
index 2d376d908f..0000000000
--- a/bootcamp_content/projects/wordle/introduction.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Maze
-
-## Overview
-
-This project contains the first exercise you ever do, but continues all the way through into Part 2.
-
-Your job is to solve, and then later create a maze that your character can explore through.
diff --git a/test/javascript/interpreter/interpreter.test.ts b/test/javascript/interpreter/interpreter.test.ts
index b0d8d5b9b4..b6e13d0062 100644
--- a/test/javascript/interpreter/interpreter.test.ts
+++ b/test/javascript/interpreter/interpreter.test.ts
@@ -153,7 +153,7 @@ describe('statements', () => {
})
describe('strings', () => {
- test('plus', () => {
+ test.skip('plus', () => {
const { frames } = interpret('set x to "sw" + "eet" ')
expect(frames).toBeArrayOfSize(1)
expect(frames[0].status).toBe('SUCCESS')
@@ -268,7 +268,7 @@ describe('statements', () => {
expect(frames[3].variables).toMatchObject({ pos: 25 })
})
})
- describe('function', () => {
+ describe('creating function', () => {
describe('without parameters', () => {
test('define', () => {
const { frames } = interpret(`
@@ -787,7 +787,7 @@ describe('evaluateFunction', () => {
expect(frames).toBeArrayOfSize(11)
})
- test('twoFer', () => {
+ test.skip('twoFer', () => {
const code = `
function twoFer with name do
if(name is "") do
diff --git a/test/javascript/interpreter/runtimeErrors.test.ts b/test/javascript/interpreter/runtimeErrors.test.ts
new file mode 100644
index 0000000000..eb4ed9e46c
--- /dev/null
+++ b/test/javascript/interpreter/runtimeErrors.test.ts
@@ -0,0 +1,114 @@
+import { interpret } from '@/interpreter/interpreter'
+import { Location, Span } from '@/interpreter/location'
+import { changeLanguage } from '@/interpreter/translator'
+import { context } from 'msw'
+
+beforeAll(() => {
+ changeLanguage('system')
+})
+
+afterAll(() => {
+ changeLanguage('en')
+})
+
+const location = new Location(0, new Span(0, 0), new Span(0, 0))
+const getNameFunction = {
+ name: 'get_name',
+ func: (_interpreter: any) => {
+ return 'Jeremy'
+ },
+ description: '',
+}
+
+function expectFrameToBeError(frame, code, type) {
+ expect(frame.code).toBe(code)
+ expect(frame.status).toBe('ERROR')
+ expect(frame.error).not.toBeNull()
+ expect(frame.error!.category).toBe('RuntimeError')
+ expect(frame.error!.type).toBe(type)
+}
+
+describe('Runtime errors', () => {
+ describe('OperandsMustBeNumbers', () => {
+ test('1 - "a"', () => {
+ const code = '1 - "a"'
+ const { frames } = interpret(code)
+ expectFrameToBeError(frames[0], code, 'OperandsMustBeNumbers')
+ expect(frames[0].error!.message).toBe(
+ 'OperandsMustBeNumbers: operator: -, side: right, value: `a`'
+ )
+ })
+ test('1 / true', () => {
+ const code = '1 / true'
+ const { frames } = interpret(code)
+ expectFrameToBeError(frames[0], code, 'OperandsMustBeNumbers')
+ expect(frames[0].error!.message).toBe(
+ 'OperandsMustBeNumbers: operator: /, side: right, value: `true`'
+ )
+ })
+ test('false - 1', () => {
+ const code = 'false - 1'
+ const { frames } = interpret(code)
+ expectFrameToBeError(frames[0], code, 'OperandsMustBeNumbers')
+ expect(frames[0].error!.message).toBe(
+ 'OperandsMustBeNumbers: operator: -, side: left, value: `false`'
+ )
+ })
+ test('1 * false', () => {
+ const code = '1 * false'
+ const { frames } = interpret(code)
+ expectFrameToBeError(frames[0], code, 'OperandsMustBeNumbers')
+ expect(frames[0].error!.message).toBe(
+ 'OperandsMustBeNumbers: operator: *, side: right, value: `false`'
+ )
+ })
+ test('1 * getName()', () => {
+ const code = '1 * get_name()'
+ const context = { externalFunctions: [getNameFunction] }
+ const { frames } = interpret(code, context)
+ expectFrameToBeError(frames[0], code, 'OperandsMustBeNumbers')
+ expect(frames[0].error!.message).toBe(
+ 'OperandsMustBeNumbers: operator: *, side: right, value: `Jeremy`'
+ )
+ })
+ })
+
+ describe('UnexpectedUncalledFunction', () => {
+ test('in a equation with a +', () => {
+ const code = 'get_name + 1'
+ const context = { externalFunctions: [getNameFunction] }
+ const { frames } = interpret(code, context)
+ expectFrameToBeError(frames[0], code, 'UnexpectedUncalledFunction')
+ expect(frames[0].error!.message).toBe('UnexpectedUncalledFunction')
+ })
+ test('in a equation with a -', () => {
+ const code = 'get_name - 1'
+ const context = { externalFunctions: [getNameFunction] }
+ const { frames } = interpret(code, context)
+ expectFrameToBeError(frames[0], code, 'UnexpectedUncalledFunction')
+ expect(frames[0].error!.message).toBe('UnexpectedUncalledFunction')
+ })
+ })
+ describe('FunctionAlreadyDeclared', () => {
+ test('basic', () => {
+ const code = 'set get_name to 5'
+ const context = { externalFunctions: [getNameFunction] }
+ const { frames } = interpret(code, context)
+ expectFrameToBeError(frames[0], code, 'FunctionAlreadyDeclared')
+ expect(frames[0].error!.message).toBe(
+ 'FunctionAlreadyDeclared: name: get_name'
+ )
+ })
+ })
+ describe('UnexpectedChangeOfFunction', () => {
+ test('basic', () => {
+ const code = 'change get_name to 5'
+ const context = { externalFunctions: [getNameFunction] }
+ const { frames } = interpret(code, context)
+ expectFrameToBeError(frames[0], code, 'UnexpectedChangeOfFunction')
+ expect(frames[0].error!.message).toBe(
+ 'UnexpectedChangeOfFunction: name: get_name'
+ )
+ })
+ })
+})