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

Lox Showcase #162

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
eedeebf
fixed showcased + added lox showcase
Jul 8, 2023
613f2ab
changed the lox showcase description
Jul 9, 2023
f6dd30a
fixed tailwind and added error handling to lox
Jul 10, 2023
a4c91ae
added a beta tag
Jul 12, 2023
a97fbca
oger
Jul 12, 2023
105b566
extended the lox example
Jul 12, 2023
308d1f0
fixed beta tag
Jul 17, 2023
5fee4ef
added autoscroll
Jul 17, 2023
23ca2fe
added debugging code
Jul 17, 2023
ebf8a84
removed autoscroll
Jul 18, 2023
3a9437c
added autoscroll (again)
Jul 18, 2023
fb8abda
removed debugging stuff from lox
Jul 18, 2023
a365a85
cleaned overflow-hidden
Aug 7, 2023
9de599f
squash and rebase
Oct 12, 2023
568a5e1
fixed showcased + added lox showcase
Jul 8, 2023
1e5beba
changed the lox showcase description
Jul 9, 2023
9e4f0a3
fixed tailwind and added error handling to lox
Jul 10, 2023
37af827
added a beta tag
Jul 12, 2023
aee2c7c
oger
Jul 12, 2023
1686025
extended the lox example
Jul 12, 2023
a5e0931
fixed beta tag
Jul 17, 2023
e06afd6
added autoscroll
Jul 17, 2023
7bf2248
added debugging code
Jul 17, 2023
772f9d7
removed autoscroll
Jul 18, 2023
bb01cf3
added autoscroll (again)
Jul 18, 2023
12680dd
removed debugging stuff from lox
Jul 18, 2023
4806fed
cleaned overflow-hidden
Aug 7, 2023
215359d
squash and rebase
Oct 12, 2023
2d757bc
Merge branch 'addLoxShowcase' of https://github.com/eclipse-langium/l…
Oct 12, 2023
57a150d
package-lock.json
Oct 12, 2023
47c22d0
fixed showcased + added lox showcase
Jul 8, 2023
ae68335
added a beta tag
Jul 12, 2023
77b3908
oger
Jul 12, 2023
fbce6a5
squash and rebase
Oct 12, 2023
11271e9
package-lock.json
Oct 12, 2023
0a7d50c
Merge branch 'addLoxShowcase' of https://github.com/eclipse-langium/l…
Oct 12, 2023
42f40be
fixed *-lock.json
Oct 12, 2023
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
2 changes: 1 addition & 1 deletion hugo/assets/scripts/arithmetics/arithmetics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class App extends React.Component<{}, AppState> {
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
Preview
</div>
<div className="border border-emeraldLangium h-full w-full">
<div className="border border-emeraldLangium h-full w-full overflow-hidden overflow-y-scroll">
emilkrebs marked this conversation as resolved.
Show resolved Hide resolved
<Preview ref={this.preview} focusLine={(line: number) => {
this.monacoEditor.current?.getEditorWrapper()?.getEditor()?.revealLineInCenter(line);
this.monacoEditor.current?.getEditorWrapper()?.getEditor()?.setPosition({ lineNumber: line, column: 1 });
Expand Down
72 changes: 72 additions & 0 deletions hugo/assets/scripts/lox/lox-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export interface LoxMessage {
type: LoxMessageType;
content: unknown;
Copy link
Contributor

Choose a reason for hiding this comment

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

Having unknown type is not so nice. There is a easy pattern I use:

type XType = 'A'|'B'|'C';
interface XBase {
  type: XType;
}

interface A extends XBase {
  type: 'A'; // subset of parent definition!
  content: string;
}

interface B extends XBase {
  type: 'B'; // subset of parent...
  content: number;
}

interface C extends XBase {
  type: 'C';
  content: boolean;
}
type X = A|B|C;

//usage: you can do stuff like this:
const x: X = ... //get from somewhere
if(x.type === 'A') {
   //it asserts then that x is of type A, so accessing x.content will be a string, no casts needed!!!
} else if(x.type === 'B') {
  //...x.content is number
} else if(x.type === 'C') {
  //...x.content is boolean
} else {
  assertUnreachable(x.type); //this code is then unreachable (it will be highlighted red when another type D was added)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The interpreter returns an unknown type, so I could do something like this:

export interface LoxMessage {
    type: LoxMessageType;
    content: MessageContent;
};

type MessageContent = string | number | boolean | null | any;

};

export type LoxMessageType = "notification" | "output" | "error";


export const exampleCode = `fun factorial(n: number): number {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

fun binomial(n: number, k: number): number {
return factorial(n) / (factorial(k) * factorial(n - k));
}

fun pow(x: number, n: number): number {
var result = 1;
for (var i = 0; i < n; i = i + 1) {
result = result * x;
}
return result;
}

fun mod(x: number, y: number): number {
return x - y * (x / y);
}

fun floor(x: number): number {
return x - mod(x, 1);
}

print("factorial(5) = " + factorial(5));
print("binomial(5, 2) = " + binomial(5, 2));
print("pow(2, 10) = " + pow(2, 10));
print("mod(10, 3) = " + mod(10, 3));
print("floor(3.14) = " + floor(3.14));
`;

export const syntaxHighlighting = {
keywords: [
'and', 'boolean', 'class', 'else', 'false', 'for', 'fun', 'if', 'nil', 'number', 'or', 'print', 'return', 'string', 'super', 'this', 'true', 'var', 'void', 'while'
],
operators: [
'-', ',', ';', ':', '!', '!=', '.', '*', '/', '+', '<', '<=', '=', '==', '=>', '>', '>='
],
symbols: /-|,|;|:|!|!=|\.|\(|\)|\{|\}|\*|\+|<|<=|=|==|=>|>|>=/,

tokenizer: {
initial: [
{ regex: /[_a-zA-Z][\w_]*/, action: { cases: { '@keywords': { "token": "keyword" }, '@default': { "token": "ID" } } } },
{ regex: /[0-9]+(\.[0-9]+)?/, action: { "token": "number" } },
{ regex: /"[^"]*"/, action: { "token": "string" } },
{ include: '@whitespace' },
{ regex: /@symbols/, action: { cases: { '@operators': { "token": "operator" }, '@default': { "token": "" } } } },
],
whitespace: [
{ regex: /\s+/, action: { "token": "white" } },
{ regex: /\/\*/, action: { "token": "comment", "next": "@comment" } },
{ regex: /\/\/[^\n\r]*/, action: { "token": "comment" } },
],
comment: [
{ regex: /[^\/\*]+/, action: { "token": "comment" } },
{ regex: /\*\//, action: { "token": "comment", "next": "@pop" } },
{ regex: /[\/\*]/, action: { "token": "comment" } },
],
}
};
257 changes: 257 additions & 0 deletions hugo/assets/scripts/lox/lox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import {
MonacoEditorReactComp,
} from "@typefox/monaco-editor-react/bundle";
import { buildWorkerDefinition } from "monaco-editor-workers";
import React, { createRef, useRef } from "react";
import { createRoot } from "react-dom/client";
import { Diagnostic, DocumentChangeResponse } from "../langium-utils/langium-ast";
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
import { LoxMessage, exampleCode, syntaxHighlighting } from "./lox-tools";
import { UserConfig } from "monaco-editor-wrapper";
import { createUserConfig } from "../utils";

buildWorkerDefinition(
"../../libs/monaco-editor-workers/workers",
new URL("", window.location.href).href,
false
);
let userConfig: UserConfig;

interface PreviewProps {
diagnostics?: Diagnostic[];
}

interface PreviewState {
diagnostics?: Diagnostic[];
messages: TerminalMessage[];
}

interface TerminalMessage {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the same trick as above to avoid Union type for content

type: "notification" | "error" | "output";
content: string | string[];
}

class Preview extends React.Component<PreviewProps, PreviewState> {
terminalContainer: React.RefObject<HTMLDivElement>;
constructor(props: PreviewProps) {
super(props);
this.state = {
diagnostics: props.diagnostics,
messages: [],
};

this.terminalContainer = createRef<HTMLDivElement>();
}

println(text: string) {
this.setState((state) => ({
messages: [...state.messages, { type: "output", content: text }],
}));
}

error(text: string) {
this.setState((state) => ({
messages: [...state.messages, { type: "error", content: text }],
}));
}

clear() {
this.setState({ messages: [] });
}

setDiagnostics(diagnostics: Diagnostic[]) {
this.setState({ diagnostics: diagnostics });

}

render() {
// if the code doesn't contain any errors and the diagnostics aren't warnings
if (this.state.diagnostics == null || this.state.diagnostics.filter((i) => i.severity === 1).length == 0) {

// auto scroll to bottom
const terminal = this.terminalContainer.current;
const newLine = terminal?.lastElementChild;
if (newLine && terminal) {
const rect = newLine.getBoundingClientRect();
if (rect.bottom <= terminal.getBoundingClientRect().bottom) {
newLine.scrollIntoView();
}
}

return (
<div>
<div className="text-sm flex flex-col p-4 overflow-x-hidden overflow-y-scroll" ref={this.terminalContainer}>
{this.state.messages.map((message, index) =>
<p key={index} className={message.type == "error" ? "text-base text-accentRed" : "text-white"}>{message.type == "error" ? "An error occurred: " : ""} {message.content}</p>
)}
</div>
</div>
);
}

// Show the exception
return (
<div className="flex flex-col h-full w-full p-4 justify-start items-center my-10" >
<div className="text-white border-2 border-solid border-accentRed rounded-md p-4 text-left text-sm cursor-default">
{this.state.diagnostics.filter((i) => i.severity === 1).map((diagnostic, index) =>
<details key={index}>
<summary>{`Line ${diagnostic.range.start.line}-${diagnostic.range.end.line}: ${diagnostic.message}`}</summary>
<p>Source: {diagnostic.source} | Code: {diagnostic.code}</p>
</details>
)}
</div>
</div>
);
}
}



class App extends React.Component<{}, {}> {
monacoEditor: React.RefObject<MonacoEditorReactComp>;
preview: React.RefObject<Preview>;
copyHint: React.RefObject<HTMLDivElement>;
shareButton: React.RefObject<HTMLImageElement>;
constructor(props) {
super(props);

// bind 'this' ref for callbacks to maintain parent context
this.onMonacoLoad = this.onMonacoLoad.bind(this);
this.onDocumentChange = this.onDocumentChange.bind(this);
this.copyLink = this.copyLink.bind(this);
this.monacoEditor = React.createRef();
this.preview = React.createRef();
this.copyHint = React.createRef();
this.shareButton = React.createRef();
}

/**
* Callback that is invoked when Monaco is finished loading up.
* Can be used to safely register notification listeners, retrieve data, and the like
*
* @throws Error on inability to ref the Monaco component or to get the language client
*/
onMonacoLoad() {
// verify we can get a ref to the editor
if (!this.monacoEditor.current) {
throw new Error("Unable to get a reference to the Monaco Editor");
}

// verify we can get a ref to the language client
const lc = this.monacoEditor.current
?.getEditorWrapper()
?.getLanguageClient();
if (!lc) {
throw new Error("Could not get handle to Language Client on mount");
}
this.monacoEditor.current.getEditorWrapper()?.getEditor()?.focus();
// register to receive DocumentChange notifications
lc.onNotification("browser/DocumentChange", this.onDocumentChange);
}

/**
* Callback invoked when the document processed by the LS changes
* Invoked on startup as well
* @param resp Response data
*/
onDocumentChange(resp: DocumentChangeResponse) {
// decode the received Asts
const message = JSON.parse(resp.content) as LoxMessage;
switch (message.type) {
case "notification":
switch (message.content) {
case "startInterpreter":
this.preview.current?.clear();
break;
}
break;
case "error":
this.preview.current?.error(message.content as string);
break;
case "output":
this.preview.current?.println(message.content as string);
break;
}
this.preview.current?.setDiagnostics(resp.diagnostics);
}


async copyLink() {
const code = this.monacoEditor.current?.getEditorWrapper()?.getEditor()?.getValue()!;
const url = new URL("/showcase/lox", window.origin);
url.searchParams.append("code", compressToEncodedURIComponent(code));

this.copyHint.current!.style.display = "block";
this.shareButton.current!.src = '/assets/checkmark.svg';
setTimeout(() => {
this.shareButton.current!.src = '/assets/share.svg';
this.copyHint.current!.style.display = 'none';
}, 1000);

navigator.clipboard.writeText(window.location.href);

await navigator.clipboard.writeText(url.toString());
}

componentDidMount() {
this.shareButton.current!.addEventListener('click', this.copyLink);
}

render() {
const style = {
height: "100%",
width: "100%",
};
const url = new URL(window.location.toString());
let code = url.searchParams.get("code");
if (code) {
code = decompressFromEncodedURIComponent(code);
}

return (
<div className="justify-center self-center flex flex-col md:flex-row h-full w-full">
<div className="float-left w-full h-full flex flex-col">
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
<span>Editor</span>
<div className="flex flex-row justify-end w-full h-full gap-2">
<div className="text-sm hidden" ref={this.copyHint}>Link was copied!</div>
<img src="/assets/share.svg" title="Copy URL to this grammar and content" className="inline w-4 h-4 cursor-pointer" ref={this.shareButton}></img>
</div>
</div>
<div className="wrapper relative bg-white dark:bg-gray-900 border border-emeraldLangium h-full w-full">
<MonacoEditorReactComp
ref={this.monacoEditor}
onLoad={this.onMonacoLoad}
userConfig={userConfig}
style={style}
/>
</div>
</div>
<div className="float-left w-full h-full flex flex-col" id="preview">
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
<span>Output</span>
</div>
<div className="border border-emeraldLangium h-full w-full overflow-hidden overflow-y-scroll">
<Preview ref={this.preview} />
</div>
</div>
</div>
);
}
}

export async function share(code: string): Promise<void> {
const url = new URL("/showcase/lox", window.origin);
url.searchParams.append("code", compressToEncodedURIComponent(code));
await navigator.clipboard.writeText(url.toString());
}

userConfig = createUserConfig({
languageId: 'lox',
code: exampleCode,
htmlElement: document.getElementById('root')!,
worker: '/showcase/libs/worker/loxServerWorker.js',
monarchGrammar: syntaxHighlighting
});
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(<App />);
2 changes: 1 addition & 1 deletion hugo/assets/scripts/sql/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class App extends React.Component<{}> {
<div className="w-1/2 p-4 text-white overflow-auto">
<h1 className="text-2xl">Langium/SQL</h1>
<p className="pt-2">
This is a showcase of <a className="text-emeraldLangium" href="https://github.com/langium/langium-sql" target="_blank">Langium/SQL</a>. The editor above
This is a showcase of <a className="text-emeraldLangium" href="https://github.com/eclipse-langium/langium-sql" target="_blank">Langium/SQL</a>. The editor above
is a Monaco editor driven by our SQL language server. The current setup mimics <a className="text-emeraldLangium" href="https://www.mysql.com" target="_blank">MySQL</a>.
</p>
<h2 className="text-xl pt-4 underline">Features</h2>
Expand Down
2 changes: 1 addition & 1 deletion hugo/assets/scripts/statemachine/statemachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class StateMachineComponent extends React.Component<{
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
Preview
</div>
<div className="border border-emeraldLangium h-full w-full">
<div className="border border-emeraldLangium h-full w-full overflow-hidden overflow-y-scroll">
<Preview ref={this.preview} />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions hugo/content/guides/scoping/class-member.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function test(): void {

Member based scoping like this requires not only a modification of the default scoping provider, but also some other prerequisites.
This includes adding a member call mechanism in your grammar and a rudimentary type system.
For this guide, we will use excerpts from the [langium-lox](https://github.com/langium/langium-lox) project to demonstrate how you can set this up yourself.
For this guide, we will use excerpts from the [langium-lox](https://github.com/eclipse-langium/langium-lox) project to demonstrate how you can set this up yourself.
This project implements a strongly-typed version of the [Lox language](https://craftinginterpreters.com/the-lox-language.html) from the popular book [Crafting Interpreters](https://craftinginterpreters.com/).

We'll first start with the `MemberCall` grammar rule, which references one of our `NamedElements`. These elements could be variable declarations, functions, classes or methods and fields of those classes. Additionally, we want to allow function calls on elements. Note that the grammar has no notion of whether these elements can actually be executed as functions. Instead, we always allow function calls on every named element, and simply provide validation errors in case an element is called erroneously. After parsing the first member call, we continue parsing further members as long as the input text provides us with further references to elements; which are separated by dots.
Expand Down Expand Up @@ -97,7 +97,7 @@ export class LoxScopeProvider extends DefaultScopeProvider {
}
```

When trying to compute the type of an expression, we are only interested in the final piece of the member call. However, to derive the type and scope of the final member call, we have to recursively identify the type of the previous member call. This is done by looking at the member call stored in the `previous` property and inferring its type. See [here for the full implementation of the type inference system in Lox](https://github.com/langium/langium-lox/blob/main/src/language-server/type-system/infer.ts). This kind of type inference requires scoping.
When trying to compute the type of an expression, we are only interested in the final piece of the member call. However, to derive the type and scope of the final member call, we have to recursively identify the type of the previous member call. This is done by looking at the member call stored in the `previous` property and inferring its type. See [here for the full implementation of the type inference system in Lox](https://github.com/eclipse-langium/langium-lox/blob/main/src/language-server/type-system/infer.ts). This kind of type inference requires scoping.

To illustrate this behavior a bit better, take a look at the following code snippet:

Expand Down
14 changes: 14 additions & 0 deletions hugo/content/showcase/lox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: "Lox"
weight: 500
type: langium
layout: showcase-page
url: "/showcase/lox"
img: "/assets/Langium_Lox.svg"
file: "scripts/lox/lox.tsx"
description: A tree-walk interpreter for the Lox language. It is based on the book 'Crafting Interpreters' by Bob Nystrom.
geekdochidden: true
draft: false
beta: true
noMain: true
---
2 changes: 1 addition & 1 deletion hugo/content/tutorials/building_an_extension/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In this tutorial we'll be going over how to build a VSIX extension (VSCode exten

## Setting up the Scripts

To get started, you'll want to have a language expressed in Langium, such as [Lox](https://github.com/langium/langium-lox) or [MiniLogo](https://github.com/langium/langium-minilogo). If you have been following along with these tutorials, you should already have something ready. If you don't you can also use the default language generated by the yeoman generator for Langium, presented in the [getting started](/docs/getting-started/) section.
To get started, you'll want to have a language expressed in Langium, such as [Lox](https://github.com/eclipse-langium/langium-lox) or [MiniLogo](https://github.com/eclipse-langium/langium-minilogo). If you have been following along with these tutorials, you should already have something ready. If you don't you can also use the default language generated by the yeoman generator for Langium, presented in the [getting started](/docs/getting-started/) section.

Regardless of what you're working with, you'll want to make sure you have the following scripts in your **package.json**.

Expand Down
Loading
Loading