Skip to content

Commit 0f6d32c

Browse files
authored
feat: add scoped runtime (#82)
* add scoped runtime * add tree-sitter wasm
1 parent 4e13610 commit 0f6d32c

File tree

11 files changed

+236
-4
lines changed

11 files changed

+236
-4
lines changed

ui/config-overrides.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@ module.exports = function override(config, env) {
1515
};
1616
// by default load all the languages
1717
config.plugins.push(new MonacoWebpackPlugin());
18+
config.resolve.fallback = {
19+
fs: false,
20+
path: false,
21+
};
1822
return config;
1923
};

ui/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"socket.io-client": "^4.5.3",
4242
"stompjs": "^2.3.3",
4343
"typescript": "^4.4.2",
44+
"web-tree-sitter": "^0.20.7",
4445
"web-vitals": "^2.1.0",
4546
"xterm": "^5.0.0",
4647
"xterm-addon-fit": "^0.6.0",
@@ -88,6 +89,8 @@
8889
"babel-plugin-named-exports-order": "^0.0.2",
8990
"prop-types": "^15.8.1",
9091
"react-app-rewired": "^2.2.1",
92+
"tree-sitter-javascript": "^0.19.0",
93+
"tree-sitter-python": "^0.20.1",
9194
"webpack": "^5.74.0"
9295
}
9396
}

ui/public/tree-sitter-javascript.wasm

228 KB
Binary file not shown.

ui/public/tree-sitter-python.wasm

258 KB
Binary file not shown.

ui/public/tree-sitter.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/public/tree-sitter.wasm

174 KB
Binary file not shown.

ui/src/components/Canvas.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { useApolloClient } from "@apollo/client";
5151
import { CanvasContextMenu } from "./CanvasContextMenu";
5252
import ToolBox, { ToolTypes } from "./Toolbox";
5353
import styles from "./canvas.style.js";
54+
import { analyzeCode } from "../lib/parser";
5455

5556
const nanoid = customAlphabet(nolookalikes, 10);
5657

@@ -194,7 +195,6 @@ const ScopeNode = memo<Props>(({ data, id, isConnectable }) => {
194195
function ResultBlock({ pod, id, showOutput = true }) {
195196
const store = useContext(RepoContext);
196197
if (!store) throw new Error("Missing BearContext.Provider in the tree");
197-
const wsRun = useStore(store, (state) => state.wsRun);
198198
return (
199199
<Box>
200200
{pod.result && (
@@ -277,6 +277,9 @@ const CodeNode = memo<Props>(({ data, id, isConnectable }) => {
277277
if (!store) throw new Error("Missing BearContext.Provider in the tree");
278278
// const pod = useStore(store, (state) => state.pods[id]);
279279
const wsRun = useStore(store, (state) => state.wsRun);
280+
const clearResults = useStore(store, (s) => s.clearResults);
281+
const setSymbolTable = useStore(store, (s) => s.setSymbolTable);
282+
const setPodVisibility = useStore(store, (s) => s.setPodVisibility);
280283
const ref = useRef(null);
281284
const [target, setTarget] = React.useState<any>(null);
282285
const [frame] = React.useState({
@@ -332,7 +335,18 @@ const CodeNode = memo<Props>(({ data, id, isConnectable }) => {
332335
deleteNodeById(id);
333336
break;
334337
case ToolTypes.play:
335-
wsRun(data.id);
338+
{
339+
// analyze code and set symbol table
340+
// TODO maybe put this logic elsewhere?
341+
let { ispublic, names } = analyzeCode(pod.content);
342+
setPodVisibility(id, ispublic);
343+
console.log("names", names);
344+
if (names) {
345+
setSymbolTable(data.id, names);
346+
}
347+
clearResults(data.id);
348+
wsRun(data.id);
349+
}
336350
break;
337351
case ToolTypes.layout:
338352
setLayout(layout === "bottom" ? "right" : "bottom");
@@ -357,11 +371,16 @@ const CodeNode = memo<Props>(({ data, id, isConnectable }) => {
357371
<Box
358372
sx={{
359373
border: "solid 1px #d6dee6",
374+
borderWidth: pod.ispublic ? "4px" : "1px",
360375
borderRadius: "4px",
361376
width: "100%",
362377
height: "100%",
363378
backgroundColor: "rgb(244, 246, 248)",
364-
borderColor: isEditorBlur ? "#d6dee6" : "#3182ce",
379+
borderColor: pod.ispublic
380+
? "green"
381+
: isEditorBlur
382+
? "#d6dee6"
383+
: "#3182ce",
365384
}}
366385
ref={ref}
367386
>

ui/src/lib/parser.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import Parser from "web-tree-sitter";
2+
3+
let parser: Parser | null = null;
4+
Parser.init({
5+
locateFile(scriptName: string, scriptDirectory: string) {
6+
return scriptName;
7+
},
8+
}).then(async () => {
9+
/* the library is ready */
10+
console.log("tree-sitter is ready");
11+
parser = new Parser();
12+
const Python = await Parser.Language.load("/tree-sitter-python.wasm");
13+
parser.setLanguage(Python);
14+
});
15+
16+
/**
17+
* Return a list of names defined in this code.
18+
* @param code the code
19+
* @returns a list of names defined in this code.
20+
*/
21+
export function analyzeCode(code) {
22+
let names: string[] = [];
23+
let ispublic = false;
24+
if (code.trim().startsWith("@export")) {
25+
ispublic = true;
26+
code = code.slice(7).trim();
27+
}
28+
if (!parser) {
29+
throw Error("warning: parser not ready");
30+
}
31+
let tree = parser.parse(code);
32+
tree.rootNode.children.forEach((node) => {
33+
if (node.type === "function_definition") {
34+
let name = node.firstNamedChild!.text;
35+
names.push(name);
36+
}
37+
});
38+
return { ispublic, names };
39+
}
40+
41+
/**
42+
* 1. parse the code, get: (defs, refs) to functions & variables
43+
* 2. consult symbol table to resolve them
44+
* 3. if all resolved, rewrite the code; otherwise, return null.
45+
* @param code
46+
* @param symbolTable
47+
* @returns
48+
*/
49+
export function rewriteCode(code, symbolTable) {
50+
console.log("--- rewriteCode with symbol table", symbolTable);
51+
if (!parser) {
52+
throw Error("warning: parser not ready");
53+
}
54+
let ispublic = false;
55+
if (code.trim().startsWith("@export")) {
56+
ispublic = true;
57+
code = code.slice(7).trim();
58+
}
59+
let tree = parser.parse(code);
60+
// iterate through all first-level children.
61+
let defrefs: Parser.SyntaxNode[] = [];
62+
// get all the references to variables and functions (currently just functions)
63+
let all_query = parser
64+
.getLanguage()
65+
.query(
66+
"[" +
67+
"(function_definition (identifier) @name)" +
68+
"(call (identifier) @name)" +
69+
"(module (expression_statement (identifier) @id))" +
70+
"]"
71+
);
72+
all_query.matches(tree.rootNode).forEach((match) => {
73+
let node = match.captures[0].node;
74+
defrefs.push(node);
75+
});
76+
// replace with symbol table
77+
let newcode = "";
78+
let index = 0;
79+
defrefs.forEach((node) => {
80+
newcode += code.slice(index, node.startIndex);
81+
if (node.text in symbolTable) {
82+
newcode += symbolTable[node.text];
83+
} else {
84+
console.log("warning: cannot resolve", node.text);
85+
newcode += node.text;
86+
}
87+
index = node.endIndex;
88+
});
89+
newcode += code.slice(index);
90+
return { ispublic, newcode };
91+
}

ui/src/lib/runtime.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createStore, StateCreator, StoreApi } from "zustand";
44

55
// FIXME cyclic import
66
import { RepoSlice } from "./store";
7+
import { rewriteCode } from "./parser";
78

89
function getChildExports({ id, pods }) {
910
// get all the exports and reexports. The return would be:
@@ -288,6 +289,58 @@ function getExports(content) {
288289
return { exports, reexports, content };
289290
}
290291

292+
function doRun({ id, socket, set, get }) {
293+
// 1. rewrite the code
294+
let pods = get().pods;
295+
let pod = pods[id];
296+
let code = pod.content;
297+
// get symbol tables
298+
//
299+
// TODO currently, I'm using only the symbol table from the current pod. I'll
300+
// need to get the symbol table from all the children as well.
301+
302+
// I should get symbol table of:
303+
// - all sibling nodes
304+
console.log("rewriting code ..");
305+
// console.log("my id", id, pod.symbolTable);
306+
// console.log("=== children", pods[pod.parent].children);
307+
// FIXME what if there are conflicts?
308+
let allSymbolTables = pods[pod.parent].children.map(({ id, type }) => {
309+
// FIXME make this consistent, CODE, POD, DECK, SCOPE; use enums
310+
if (pods[id].type === "CODE") {
311+
return pods[id].symbolTable || {};
312+
} else {
313+
let tables = pods[id].children
314+
.filter(({ id }) => pods[id].ispublic)
315+
.map(({ id }) => pods[id].symbolTable || {});
316+
return Object.assign({}, ...tables);
317+
}
318+
});
319+
// console.log("=== allSymbolTables", allSymbolTables);
320+
let combinedSymbolTable = Object.assign({}, ...allSymbolTables);
321+
// console.log("=== combinedSymbolTable", combinedSymbolTable);
322+
let { ispublic, newcode } = rewriteCode(code, combinedSymbolTable);
323+
get().setPodVisibility(id, ispublic);
324+
console.log("new code:\n", newcode);
325+
326+
get().setRunning(pod.id);
327+
socket.send(
328+
JSON.stringify({
329+
type: "runCode",
330+
payload: {
331+
lang: pod.lang,
332+
code: newcode,
333+
namespace: pod.ns,
334+
raw: true,
335+
podId: pod.id,
336+
sessionId: get().sessionId,
337+
},
338+
})
339+
);
340+
341+
// 2. send for evaluation
342+
}
343+
291344
function handleRunTree({ id, socket, set, get }) {
292345
// get all pods
293346
function helper(id) {
@@ -540,6 +593,7 @@ export interface RuntimeSlice {
540593
clearResults: (id) => void;
541594
clearAllResults: () => void;
542595
setRunning: (id) => void;
596+
setSymbolTable: (id: string, names: string[]) => void;
543597
addPodExport: (id, exports, reexports) => void;
544598
clearAllExports: () => void;
545599
setPodExport: ({ id, exports, reexports }) => void;
@@ -657,7 +711,7 @@ export const createRuntimeSlice: StateCreator<
657711
});
658712
return;
659713
}
660-
handleRunTree({
714+
doRun({
661715
id,
662716
socket: {
663717
send: (payload) => {
@@ -670,6 +724,7 @@ export const createRuntimeSlice: StateCreator<
670724
});
671725
},
672726
wsPowerRun: ({ id, doEval }) => {
727+
throw Error("Depcrecated");
673728
if (!get().socket) {
674729
get().addError({
675730
type: "error",
@@ -721,6 +776,19 @@ export const createRuntimeSlice: StateCreator<
721776
},
722777
// ==========
723778
// exports
779+
setSymbolTable: (id: string, names: string[]) => {
780+
set(
781+
produce((state) => {
782+
// a symbol table is foo->foo_<podid>
783+
state.pods[id].symbolTable = Object.assign(
784+
{},
785+
...names.map((name) => ({
786+
[name]: `${name}_${id}`,
787+
}))
788+
);
789+
})
790+
);
791+
},
724792
addPodExport: ({ id, name }) => {
725793
set(
726794
produce((state) => {

ui/src/lib/store.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { WebsocketProvider } from "y-websocket";
1717
import { createRuntimeSlice, RuntimeSlice } from "./runtime";
1818
import { ApolloClient } from "@apollo/client";
1919
import { addAwarenessStyle } from "./styles";
20+
import { analyzeCode } from "./parser";
2021

2122
// Tofix: can't connect to http://codepod.127.0.0.1.sslip.io/socket/, but it works well on webbrowser or curl
2223
let serverURL;
@@ -111,6 +112,8 @@ export type Pod = {
111112
fold?: boolean;
112113
thundar?: boolean;
113114
utility?: boolean;
115+
symbolTable?: { [key: string]: string };
116+
ispublic?: boolean;
114117
exports?: { [key: string]: string[] };
115118
imports?: {};
116119
reexports?: {};
@@ -191,6 +194,7 @@ export interface RepoSlice {
191194
getPod: (string) => Pod;
192195
getPods: () => Record<string, Pod>;
193196
getId2children: (string) => string[];
197+
setPodVisibility: (id, visible) => void;
194198
}
195199

196200
type BearState = RepoSlice & RuntimeSlice;
@@ -268,6 +272,8 @@ const createRepoSlice: StateCreator<
268272
thundar: false,
269273
utility: false,
270274
name: "",
275+
ispublic: false,
276+
symbolTable: {},
271277
exports: {},
272278
imports: {},
273279
reexports: {},
@@ -665,6 +671,13 @@ const createRepoSlice: StateCreator<
665671
state.pods[id].dirty = true;
666672
})
667673
),
674+
setPodVisibility: (id, ispublic) => {
675+
set(
676+
produce((state) => {
677+
state.pods[id].ispublic = ispublic;
678+
})
679+
);
680+
},
668681
loadRepo: async (client, id) => {
669682
const { pods, name } = await doRemoteLoadRepo({ id, client });
670683
set(
@@ -679,6 +692,15 @@ const createRepoSlice: StateCreator<
679692
state.id2parent[pod.id] = pod.parent.id;
680693
}
681694
state.id2children[pod.id] = pod.children.map((child) => child.id);
695+
// trigger analyze code for symbol table
696+
let { ispublic, names } = analyzeCode(pod.content);
697+
pod.ispublic = ispublic;
698+
pod.symbolTable = Object.assign(
699+
{},
700+
...names.map((name) => ({
701+
[name]: `${name}_${id}`,
702+
}))
703+
);
682704
}
683705
state.repoLoaded = true;
684706
})

0 commit comments

Comments
 (0)