Skip to content

Commit bc8d6c5

Browse files
committed
Support language and interpreter in the web (#3)
1 parent f28627c commit bc8d6c5

File tree

9 files changed

+596
-174
lines changed

9 files changed

+596
-174
lines changed

langium-config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"fileExtensions": [".lox"],
77
"textMate": {
88
"out": "syntaxes/lox.tmLanguage.json"
9+
},
10+
"monarch": {
11+
"out": "syntaxes/lox.monarch.ts"
912
}
1013
}],
1114
"out": "src/language-server/generated"

package-lock.json

Lines changed: 397 additions & 137 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,37 @@
1010
"Programming Languages"
1111
],
1212
"contributes": {
13-
"languages": [{
14-
"id": "lox",
15-
"aliases": ["Lox", "lox"],
16-
"extensions": [".lox"],
17-
"configuration": "./language-configuration.json"
18-
}],
19-
"grammars": [{
20-
"language": "lox",
21-
"scopeName": "source.lox",
22-
"path": "./syntaxes/lox.tmLanguage.json"
23-
}],
24-
"notebooks": [{
25-
"type": "lox-notebook",
26-
"displayName": "Lox Notebook",
27-
"selector": [
28-
{
29-
"filenamePattern": "*.loxnb"
30-
}
31-
]
32-
}]
13+
"languages": [
14+
{
15+
"id": "lox",
16+
"aliases": [
17+
"Lox",
18+
"lox"
19+
],
20+
"extensions": [
21+
".lox"
22+
],
23+
"configuration": "./language-configuration.json"
24+
}
25+
],
26+
"grammars": [
27+
{
28+
"language": "lox",
29+
"scopeName": "source.lox",
30+
"path": "./syntaxes/lox.tmLanguage.json"
31+
}
32+
],
33+
"notebooks": [
34+
{
35+
"type": "lox-notebook",
36+
"displayName": "Lox Notebook",
37+
"selector": [
38+
{
39+
"filenamePattern": "*.loxnb"
40+
}
41+
]
42+
}
43+
]
3344
},
3445
"activationEvents": [
3546
"onLanguage:lox"
@@ -49,18 +60,16 @@
4960
"build": "tsc -b tsconfig.json",
5061
"watch": "tsc -b tsconfig.json --watch",
5162
"lint": "eslint src --ext ts",
63+
"clean": "shx rm -rf out node_modules",
5264
"langium:generate": "langium generate",
5365
"langium:watch": "langium generate --watch"
5466
},
5567
"dependencies": {
56-
"chevrotain": "^9.1.0",
5768
"colors": "^1.4.0",
5869
"commander": "^8.0.0",
59-
"langium": "1.0.1",
70+
"langium": "~1.2.1",
6071
"uuid": "^9.0.0",
61-
"vscode-languageclient": "8.0.2",
62-
"vscode-languageserver": "8.0.2",
63-
"vscode-uri": "^3.0.2"
72+
"vscode-languageclient": "^8.1.0"
6473
},
6574
"devDependencies": {
6675
"@types/node": "^14.17.3",
@@ -69,7 +78,8 @@
6978
"@typescript-eslint/eslint-plugin": "^4.14.1",
7079
"@typescript-eslint/parser": "^4.14.1",
7180
"eslint": "^7.19.0",
72-
"langium-cli": "1.0.0",
81+
"langium-cli": "~1.2.1",
82+
"shx": "^0.3.4",
7383
"typescript": "^4.6.2"
7484
}
7585
}

src/interpreter/runner.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { BinaryExpression, Expression, isBinaryExpression, isBooleanExpression,
33
import { createLoxServices } from "../language-server/lox-module";
44
import { v4 } from 'uuid';
55
import { URI } from "vscode-uri";
6-
import { CancellationToken } from "vscode-languageclient";
6+
import { CancellationToken, CancellationTokenSource } from "vscode-languageserver";
77

88
export interface InterpreterContext {
9-
log: (value: unknown) => MaybePromise<void>
9+
log: (value: unknown) => MaybePromise<void>,
10+
onStart?: () => void,
1011
}
1112

1213
const services = createLoxServices(EmptyFileSystem);
1314

15+
// after 5 seconds, the interpreter will be interrupted and call onTimeout
16+
const TIMEOUT_MS = 1000 * 5;
17+
1418
export async function runInterpreter(program: string, context: InterpreterContext): Promise<void> {
1519
const buildResult = await buildDocument(program);
1620
try {
@@ -25,7 +29,10 @@ type ReturnFunction = (value: unknown) => void;
2529

2630
interface RunnerContext {
2731
variables: Variables,
28-
log: (value: unknown) => MaybePromise<void>
32+
cancellationToken: CancellationToken,
33+
timeout: NodeJS.Timeout,
34+
log: (value: unknown) => MaybePromise<void>,
35+
onStart?: () => void,
2936
}
3037

3138
class Variables {
@@ -88,17 +95,38 @@ async function buildDocument(program: string): Promise<BuildResult> {
8895
}
8996
}
9097

91-
async function runProgram(program: LoxProgram, outerContext: InterpreterContext): Promise<void> {
98+
export async function runProgram(program: LoxProgram, outerContext: InterpreterContext): Promise<void> {
99+
const cancellationTokenSource = new CancellationTokenSource();
100+
const cancellationToken = cancellationTokenSource.token;
101+
102+
const timeout = setTimeout(async () => {
103+
cancellationTokenSource.cancel();
104+
}, TIMEOUT_MS);
105+
92106
const context: RunnerContext = {
93107
variables: new Variables(),
94-
log: outerContext.log
108+
cancellationToken,
109+
timeout,
110+
log: outerContext.log,
111+
onStart: outerContext.onStart,
95112
};
96-
context.variables.enter();
113+
97114
let end = false;
115+
116+
context.variables.enter();
117+
if (context.onStart) {
118+
context.onStart();
119+
}
120+
98121
for (const statement of program.elements) {
122+
await interruptAndCheck(context.cancellationToken);
123+
99124
if (!isClass(statement) && !isFunctionDeclaration(statement)) {
100125
await runLoxElement(statement, context, () => { end = true });
101126
}
127+
else if (isClass(statement)) {
128+
throw new AstNodeError(statement, 'Classes are currently unsupported');
129+
}
102130
if (end) {
103131
break;
104132
}
@@ -107,6 +135,8 @@ async function runProgram(program: LoxProgram, outerContext: InterpreterContext)
107135
}
108136

109137
async function runLoxElement(element: LoxElement, context: RunnerContext, returnFn: ReturnFunction): Promise<void> {
138+
await interruptAndCheck(context.cancellationToken);
139+
110140
if (isExpressionBlock(element)) {
111141
await interruptAndCheck(CancellationToken.None);
112142
context.variables.enter();
@@ -164,6 +194,9 @@ async function runLoxElement(element: LoxElement, context: RunnerContext, return
164194
}
165195

166196
async function runExpression(expression: Expression, context: RunnerContext): Promise<unknown> {
197+
await interruptAndCheck(context.cancellationToken);
198+
199+
167200
if (isBinaryExpression(expression)) {
168201
const { left, right, operator } = expression;
169202
const rightValue = await runExpression(right, context);
@@ -244,6 +277,8 @@ async function setExpressionValue(left: Expression, right: unknown, context: Run
244277
}
245278

246279
async function runMemberCall(memberCall: MemberCall, context: RunnerContext): Promise<unknown> {
280+
await interruptAndCheck(context.cancellationToken);
281+
247282
let previous: unknown = undefined;
248283
if (memberCall.previous) {
249284
previous = await runExpression(memberCall.previous, context);
@@ -255,7 +290,7 @@ async function runMemberCall(memberCall: MemberCall, context: RunnerContext): Pr
255290
} else if (isVariableDeclaration(ref) || isParameter(ref)) {
256291
value = context.variables.get(memberCall, ref.name);
257292
} else if (isClass(ref)) {
258-
throw new AstNodeError(memberCall, 'Classes are current unsupported');
293+
throw new AstNodeError(memberCall, 'Classes are currently unsupported');
259294
} else {
260295
value = previous;
261296
}

src/language-server/lox-validator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AstNode, streamAllContents, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium';
2-
import { BinaryExpression, ExpressionBlock, FunctionDeclaration, isReturnStatement, LoxAstType, MethodMember, TypeReference, UnaryExpression, VariableDeclaration } from './generated/ast';
2+
import { BinaryExpression, Class, ExpressionBlock, FunctionDeclaration, isReturnStatement, LoxAstType, MethodMember, TypeReference, UnaryExpression, VariableDeclaration } from './generated/ast';
33
import type { LoxServices } from './lox-module';
44
import { isAssignable } from './type-system/assignment';
55
import { isVoidType, TypeDescription, typeToString } from './type-system/descriptions';
@@ -18,6 +18,7 @@ export class LoxValidationRegistry extends ValidationRegistry {
1818
UnaryExpression: validator.checkUnaryOperationAllowed,
1919
VariableDeclaration: validator.checkVariableDeclaration,
2020
MethodMember: validator.checkMethodReturnType,
21+
Class: validator.checkClassDeclaration,
2122
FunctionDeclaration: validator.checkFunctionReturnType
2223
};
2324
this.register(checks, validator);
@@ -37,6 +38,14 @@ export class LoxValidator {
3738
this.checkFunctionReturnTypeInternal(method.body, method.returnType, accept);
3839
}
3940

41+
// TODO: implement classes
42+
checkClassDeclaration(declaration: Class, accept: ValidationAcceptor): void {
43+
accept('error', 'Classes are currently unsupported.', {
44+
node: declaration,
45+
property: 'name'
46+
});
47+
}
48+
4049
private checkFunctionReturnTypeInternal(body: ExpressionBlock, returnType: TypeReference, accept: ValidationAcceptor): void {
4150
const map = this.getTypeCache();
4251
const returnStatements = streamAllContents(body).filter(isReturnStatement).toArray();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/******************************************************************************
2+
* Copyright 2022 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import { startLanguageServer, EmptyFileSystem, DocumentState, LangiumDocument, OperationCancelled } from 'langium';
8+
import { BrowserMessageReader, BrowserMessageWriter, Diagnostic, NotificationType, createConnection } from 'vscode-languageserver/browser';
9+
import { runInterpreter } from '../interpreter/runner';
10+
import { createLoxServices } from './lox-module';
11+
12+
declare const self: DedicatedWorkerGlobalScope;
13+
14+
/* browser specific setup code */
15+
const messageReader = new BrowserMessageReader(self);
16+
const messageWriter = new BrowserMessageWriter(self);
17+
18+
const connection = createConnection(messageReader, messageWriter);
19+
20+
// Inject the shared services and language-specific services
21+
const { shared } = createLoxServices({ connection, ...EmptyFileSystem });
22+
23+
// Start the language server with the shared services
24+
startLanguageServer(shared);
25+
26+
// Send a notification with the serialized AST after every document change
27+
type DocumentChange = { uri: string, content: string, diagnostics: Diagnostic[] };
28+
const documentChangeNotification = new NotificationType<DocumentChange>('browser/DocumentChange');
29+
30+
31+
shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Validated, async documents => {
32+
for (const document of documents) {
33+
if (document.diagnostics === undefined || document.diagnostics.filter((i) => i.severity === 1).length === 0) {
34+
try {
35+
await runInterpreter(document.textDocument.getText(), {
36+
log: (message) => {
37+
sendMessage(document, "output", message);
38+
},
39+
onStart: () => {
40+
sendMessage(document, "notification", "startInterpreter");
41+
},
42+
});
43+
} catch (e: any) {
44+
if (e === OperationCancelled) {
45+
sendMessage(document, "error", "Interpreter timed out");
46+
} else {
47+
sendMessage(document, "error", e.message);
48+
}
49+
} finally {
50+
sendMessage(document, "notification", "endInterpreter");
51+
}
52+
53+
}
54+
else {
55+
sendMessage(document, "error", document.diagnostics)
56+
}
57+
}
58+
});
59+
60+
function sendMessage(document: LangiumDocument, type: string, content: unknown): void {
61+
connection.sendNotification(documentChangeNotification, {
62+
uri: document.uri.toString(),
63+
content: JSON.stringify({ type, content }),
64+
diagnostics: document.diagnostics ?? []
65+
});
66+
}

syntaxes/lox.monarch.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Monarch syntax highlighting for the lox language.
2+
export default {
3+
keywords: [
4+
'and','boolean','class','else','false','for','fun','if','nil','number','or','print','return','string','super','this','true','var','void','while'
5+
],
6+
operators: [
7+
'!','!=','*','+',',','-','.','/',':',';','<','<=','=','==','=>','>','>='
8+
],
9+
symbols: /!|!=|\(|\)|\*|\+|,|-|\.|/|:|;|<|<=|=|==|=>|>|>=|\{|\}/,
10+
11+
tokenizer: {
12+
initial: [
13+
{ regex: /[_a-zA-Z][\w_]*/, action: { cases: { '@keywords': {"token":"keyword"}, '@default': {"token":"ID"} }} },
14+
{ regex: /[0-9]+(\.[0-9]+)?/, action: {"token":"number"} },
15+
{ regex: /"[^"]*"/, action: {"token":"string"} },
16+
{ include: '@whitespace' },
17+
{ regex: /@symbols/, action: { cases: { '@operators': {"token":"operator"}, '@default': {"token":""} }} },
18+
],
19+
whitespace: [
20+
{ regex: /\s+/, action: {"token":"white"} },
21+
{ regex: /\/\*/, action: {"token":"comment","next":"@comment"} },
22+
{ regex: /\/\/[^\n\r]*/, action: {"token":"comment"} },
23+
],
24+
comment: [
25+
{ regex: /[^\/\*]+/, action: {"token":"comment"} },
26+
{ regex: /\*\//, action: {"token":"comment","next":"@pop"} },
27+
{ regex: /[\/\*]/, action: {"token":"comment"} },
28+
],
29+
}
30+
};

syntaxes/lox.tmLanguage.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
{
1616
"name": "string.quoted.double.lox",
1717
"begin": "\"",
18-
"end": "\""
18+
"end": "\"",
19+
"patterns": [
20+
{
21+
"include": "#string-character-escape"
22+
}
23+
]
1924
}
2025
],
2126
"repository": {
@@ -47,6 +52,10 @@
4752
"name": "comment.line.lox"
4853
}
4954
]
55+
},
56+
"string-character-escape": {
57+
"name": "constant.character.escape.lox",
58+
"match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)"
5059
}
5160
}
52-
}
61+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"target": "ES6",
44
"module": "commonjs",
5-
"lib": ["ESNext"],
5+
"lib": ["ESNext", "WebWorker"],
66
"sourceMap": true,
77
"outDir": "out",
88
"strict": true,

0 commit comments

Comments
 (0)