diff --git a/.vscode/launch.json b/.vscode/launch.json index 80f02b8..8de82fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,6 +27,17 @@ "${workspaceFolder}/out/**/*.js", "${workspaceFolder}/node_modules/langium/**/*.js" ] + }, + { + "type": "node", + "request": "launch", + "name": "Debug Current Test File", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal" } ] } diff --git a/package.json b/package.json index e133faa..d1755ea 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "vscode:prepublish": "npm run build && npm run lint", "build": "tsc -b tsconfig.json", "watch": "tsc -b tsconfig.json --watch", - "lint": "eslint src --ext ts", + "test": "vitest run", + "lint": "eslint src test --ext ts", "langium:generate": "langium generate", "langium:watch": "langium generate --watch" }, @@ -70,6 +71,7 @@ "@typescript-eslint/parser": "^4.14.1", "eslint": "^7.19.0", "langium-cli": "1.0.0", - "typescript": "^4.6.2" + "typescript": "^4.6.2", + "vitest": "^0.34.2" } } diff --git a/src/interpreter/runner.ts b/src/interpreter/runner.ts index 76a8b2e..f69e558 100644 --- a/src/interpreter/runner.ts +++ b/src/interpreter/runner.ts @@ -3,7 +3,7 @@ import { BinaryExpression, Expression, isBinaryExpression, isBooleanExpression, import { createLoxServices } from "../language-server/lox-module"; import { v4 } from 'uuid'; import { URI } from "vscode-uri"; -import { CancellationToken } from "vscode-languageclient"; +import { CancellationToken } from 'vscode-jsonrpc'; export interface InterpreterContext { log: (value: unknown) => MaybePromise @@ -261,10 +261,10 @@ async function runMemberCall(memberCall: MemberCall, context: RunnerContext): Pr } if (memberCall.explicitOperationCall) { - if (isFunctionDeclaration(ref)) { + if (isFunctionDeclaration(value)) { const args = await Promise.all(memberCall.arguments.map(e => runExpression(e, context))); context.variables.enter(); - const names = ref.parameters.map(e => e.name); + const names = value.parameters.map(e => e.name); for (let i = 0; i < args.length; i++) { context.variables.push(names[i], args[i]); } @@ -272,7 +272,7 @@ async function runMemberCall(memberCall: MemberCall, context: RunnerContext): Pr const returnFn: ReturnFunction = (returnValue) => { functionValue = returnValue; } - await runLoxElement(ref.body, context, returnFn); + await runLoxElement(value.body, context, returnFn); context.variables.leave(); return functionValue; } else { diff --git a/test/function.test.ts b/test/function.test.ts new file mode 100644 index 0000000..44d7d31 --- /dev/null +++ b/test/function.test.ts @@ -0,0 +1,118 @@ +import { runInterpreter } from '../src/interpreter/runner.js'; +import { expect, test } from 'vitest'; + +test('identity function', async() => { + const input = ` + fun returnSum(a: number, b: number): number { + print "returnSum called"; + return a + b; + } + + fun identity(a: (number, number) => number): (number, number) => number { + print "identity called"; + return a; + } + + print identity(returnSum)(27, 15); // prints "42"; + `; + + const expectedOutput = ` + identity called + returnSum called + 42 + `; + + await runInterpreterAndAssertOutput(input, expectedOutput); +}); + +test('pass reference to function and call it', async() => { + const input = ` + fun aFunction(aLambda: (number) => number, aNumber: number): number { + print "aFunction called"; + return aLambda(aNumber); + } + + fun aTimesTwo(a: number): number { + print "aTimeTwo called"; + return a * 2; + } + + var result = aFunction(aTimesTwo, 9); + print result; + `; + + const expectedOutput = ` + aFunction called + aTimeTwo called + 18 + `; + + await runInterpreterAndAssertOutput(input, expectedOutput); +}); + +test('Closure 1', async() => { + const input = ` + // So far fails with: No variable 'outside' defined + + fun returnFunction(): () => void { + var outside = "outside"; + + fun inner(): void { + print outside; + } + + return inner; + } + + var fn = returnFunction(); + fn(); + `; + + const expectedOutput = ` + `; + + await runInterpreterAndAssertOutput(input, expectedOutput); +}); + +test('Closure 2', async() => { + const input = ` + // So far fails with: No variable 'exponent' defined + + fun power(exponent: number): (number) => number { + fun applyPower(base: number): number { + var current = 1; + for (var i = 0; i < exponent; i = i + 1) { + current = current * base; + } + return current; + } + return applyPower; + } + + var cube = power(3); + + print cube(1); + print cube(2); + print cube(3); + print cube(4); + print cube(5); + `; + + const expectedOutput = ` + `; + + await runInterpreterAndAssertOutput(input, expectedOutput); +}); + + +async function runInterpreterAndAssertOutput(input: string, expectedOutput: string) { + // TODO call valication before ?!? + let output = ""; + await runInterpreter(input, { + log: value => { + output = output.concat(`${value}`); + } + }); + expect(output.replace(/\s/g, "")).toBe(expectedOutput.replace(/\s/g, "")); +} + diff --git a/test/grammar-parse.test.ts b/test/grammar-parse.test.ts new file mode 100644 index 0000000..b49e1cd --- /dev/null +++ b/test/grammar-parse.test.ts @@ -0,0 +1,28 @@ +import type { LoxProgram } from '../src/language-server/generated/ast.js'; +import { createLoxServices } from '../src/language-server/lox-module.js'; +import { EmptyFileSystem } from 'langium'; +import { parseHelper } from 'langium/test'; +import { test } from 'vitest'; + + +test('parse', async() => { + const services = createLoxServices(EmptyFileSystem).Lox; + const parse = parseHelper(services); + + const input = ` + fun returnSum(a: number, b: number): number { + return a + b; + } + + // Closures + + fun identity(a: (number, number) => number): (number, number) => number { + return a; + } + + print identity(returnSum)(1, 2); // prints "3"; + ` + + const ast = await parse(input); + ast.parseResult; +}); diff --git a/tsconfig.json b/tsconfig.json index ca965e3..3d771e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "forceConsistentCasingInFileNames": true }, "include": [ - "src/**/*.ts" + "src/**/*", + "test/**/*" ], "exclude": [ "out", diff --git a/tsconfig.src.json b/tsconfig.src.json new file mode 100644 index 0000000..fa0c6ea --- /dev/null +++ b/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "out" + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..13d368d --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "test" + }, + "references": [{ + "path": "./tsconfig.src.json" + }], + "include": [ + "test/**/*", + ] + } \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..015d143 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['src'], + exclude: ['**/generated'], + }, + deps: { + interopDefault: true + }, + include: ['test/*.test.ts'] + } +}) \ No newline at end of file