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

fixed using function as argument in lox interpreter #5

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
1 change: 1 addition & 0 deletions examples/closures.loxnb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells":[{"kind":2,"language":"lox","value":"// So far fails with: No variable 'outside' defined\n\nfun returnFunction(): () => void {\n var outside = \"outside\";\n\n fun inner(): void {\n print outside;\n }\n\n return inner;\n}\n\nvar fn = returnFunction();\nfn();"},{"kind":2,"language":"lox","value":"// So far fails with: No variable 'exponent' defined\n\nfun power(exponent: number): (number) => number {\n\tfun applyPower(base: number): number {\n \tvar current = 1;\n for (var i = 0; i < exponent; i = i + 1) {\n \tcurrent = current * base;\n }\n return current;\n }\n return applyPower;\n}\n\nvar cube = power(3);\n\nprint cube(1);\nprint cube(2);\nprint cube(3);\nprint cube(4);\nprint cube(5);\n"}]}
tkueeu marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions examples/first-class-function.loxnb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells":[{"kind":2,"language":"lox","value":"fun returnSum(a: number, b: number): number {\n print (\"returnSum\");\n return a + b;\n}\n\n// Closures\n\nfun identity(a: (number, number) => number): (number, number) => number {\n print (\"identity\");\n return a;\n}\n\n// Calls identity with reference to function as argument, which is returned,\n// and apply (1, 2) on that function, which\n// Obviously identity is called before returnSum.\nprint identity(returnSum)(1, 2); \n\n// Calls returnSum, then identity is called with that return value as argument.\n// Obviously returnSum is called before identity.\n// print identity(returnSum(1, 2)); \n// But: This should fail, as the number return value is no valid argument for a: (number, number) => number !!!\n\n\n// var av = 1;\n// av();\n// Expected: Cannot call a non-function\n"},{"kind":2,"language":"lox","value":"fun aFunction(aLambda: (number) => number, aNumber: number): number {\n return aLambda(aNumber);\n}\n\nfun aTimesTwo(a: number): number {\n return a * 2;\n}\n\nvar result = aFunction(aTimesTwo, 9);\nprint result;"}]}
30 changes: 16 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@
],
"contributes": {
"languages": [{
"id": "lox",
"id": "lox",
"aliases": ["Lox", "lox"],
"extensions": [".lox"],
"configuration": "./language-configuration.json"
"configuration": "./language-configuration.json"
}],
"grammars": [{
"language": "lox",
"scopeName": "source.lox",
"path": "./syntaxes/lox.tmLanguage.json"
"language": "lox",
"scopeName": "source.lox",
"path": "./syntaxes/lox.tmLanguage.json"
}],
"notebooks": [{
"type": "lox-notebook",
"displayName": "Lox Notebook",
"selector": [
{
"filenamePattern": "*.loxnb"
}
]
"type": "lox-notebook",
"displayName": "Lox Notebook",
"selector": [
{
"filenamePattern": "*.loxnb"
}
]
}]
},
"activationEvents": [
Expand All @@ -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"
},
Expand All @@ -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"
}
}
8 changes: 4 additions & 4 deletions src/interpreter/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was necessary to import less from vscode, otherwise the full initialization breaks unit test


export interface InterpreterContext {
log: (value: unknown) => MaybePromise<void>
Expand Down Expand Up @@ -261,18 +261,18 @@ 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]);
}
let functionValue: unknown;
const returnFn: ReturnFunction = (returnValue) => {
functionValue = returnValue;
}
await runLoxElement(ref.body, context, returnFn);
await runLoxElement(value.body, context, returnFn);
context.variables.leave();
return functionValue;
} else {
Expand Down
118 changes: 118 additions & 0 deletions test/function.test.ts
Original file line number Diff line number Diff line change
@@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe failing tests should be commented out and marked with a TODO or FIXME comment?


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, ""));
}

28 changes: 28 additions & 0 deletions test/grammar-parse.test.ts
Original file line number Diff line number Diff line change
@@ -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<LoxProgram>(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;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not yet a complete test ...

});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
"src/**/*",
"test/**/*"
],
"exclude": [
"out",
Expand Down
10 changes: 10 additions & 0 deletions tsconfig.src.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "out"
},
"include": [
"src/**/*"
]
}
13 changes: 13 additions & 0 deletions tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.src.json",
"compilerOptions": {
"noEmit": true,
"rootDir": "test"
},
"references": [{
"path": "./tsconfig.src.json"
}],
"include": [
"test/**/*",
]
}
16 changes: 16 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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']
}
})