Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix OperandsMustBeNumbers #7379

Merged
merged 6 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/javascript/interpreter/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ export type RuntimeErrorType =
| 'UnexpectedEqualsForEquality'
| 'VariableAlreadyDeclared'
| 'VariableNotDeclared'
| 'UnexpectedUncalledFunction'
| 'FunctionAlreadyDeclared'
| 'UnexpectedChangeOfFunction'

export type StaticErrorType =
| DisabledLanguageFeatureErrorType
Expand Down
174 changes: 144 additions & 30 deletions app/javascript/interpreter/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
UnaryExpression,
UpdateExpression,
VariableExpression,
ExpressionWithValue,
} from './expression'
import { Location, Span } from './location'
import {
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -678,16 +720,32 @@ 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 {
...result,
value: plusValue2DP,
}
}
if (isString(left.value) && isString(right.value))
/*if (isString(left.value) && isString(right.value))
return {
...result,
value: left.value + right.value,
Expand All @@ -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, {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/interpreter/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>change</code> 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.",
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/interpreter/locales/system/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand Down
6 changes: 0 additions & 6 deletions bootcamp_content/projects/two-fer/config.json

This file was deleted.

47 changes: 0 additions & 47 deletions bootcamp_content/projects/two-fer/exercises/basic/config.json

This file was deleted.

This file was deleted.

Loading
Loading