Skip to content

Commit

Permalink
Remove resolver and rework the Interpreter (#7360)
Browse files Browse the repository at this point in the history
* WIP

* Remove Javascript interpreter

* Add tests for all exercises

* Tidy imports

* Tidy imports
  • Loading branch information
iHiD authored Jan 22, 2025
1 parent 30e96fa commit 01f0351
Show file tree
Hide file tree
Showing 68 changed files with 588 additions and 6,133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export function useDrawingEditorHandler() {
// value is studentCode
const evaluated = interpret(value, {
externalFunctions: drawExerciseInstance?.availableFunctions,
language: 'JikiScript',
})

const { frames } = evaluated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function useEditorHandler({
try {
runCode(value, editorViewRef.current)
} catch (e: unknown) {
console.log(e)
setHasUnhandledError(true)
setUnhandledErrorBase64(
JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export default class DigitalClockExercise extends Exercise {
}

public getState() {
console.log(this.displayedTime)
return { displayedTime: this.displayedTime }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export function useConstructRunCode({

const context = {
externalFunctions: exercise?.availableFunctions,
language: 'JikiScript',
languageFeatures: config.interpreterOptions,
}
// @ts-ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export function execGenericTest(

const evaluated = evaluateFunction(
options.studentCode,
{
language: 'JikiScript',
},
{},
testData.function,
...params
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export function execProjectTest(

const context = {
externalFunctions: exercise.availableFunctions,
language: 'JikiScript',
languageFeatures: options.config.interpreterOptions,
}
let evaluated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
})
}
4 changes: 2 additions & 2 deletions app/javascript/components/bootcamp/types/Matchers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ declare type AvailableMatchers =
| 'toBe'
| 'toBeTrue'
| 'toBeFalse'
| 'toExist'
| 'toNotExist'
| 'toBeDefined'
| 'toBeUndefined'
| 'toEqual'
| 'toBeGreaterThanOrEqual'
| 'toBeLessThanOrEqual'
Expand Down
33 changes: 15 additions & 18 deletions app/javascript/interpreter/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { isString } from './checks'

export class Environment {
private readonly values: Map<string, any> = 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
Expand All @@ -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())
Expand All @@ -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', {
Expand All @@ -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<string, any> {
let current: Environment | null = this
let vars: any = {}
Expand Down
71 changes: 71 additions & 0 deletions app/javascript/interpreter/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -37,6 +107,7 @@ export type RuntimeErrorType =
| 'InvalidIndexGetterTarget'
| 'InvalidIndexSetterTarget'
| 'UnexpectedEqualsForEquality'
| 'VariableAlreadyDeclared'

export type StaticErrorType =
| DisabledLanguageFeatureErrorType
Expand Down
Loading

0 comments on commit 01f0351

Please sign in to comment.