diff --git a/astro.config.mjs b/astro.config.mjs index b44fc794..9b530e3c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -362,6 +362,34 @@ export default defineConfig({ directory: "products/splashkit/documentation/expansions", }, }, + { + label: "SplashKit Online", + autogenerate: { + directory: "products/splashkit/documentation/splashkit-online", + }, + items: [ + { + label: "Code Documentation", + autogenerate: { + directory: "products/splashkit/documentation/splashkit-online/code-documentation", + }, + items: [ + { + label: "Classes", + autogenerate: { + directory: "products/splashkit/documentation/splashkit-online/code-documentation/classes", + }, + }, + { + label: "Processes", + autogenerate: { + directory: "products/splashkit/documentation/splashkit-online/code-documentation/processes", + }, + }, + ] + }, + ] + }, ], }, { diff --git a/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/ExecutionEnvironment.md b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/ExecutionEnvironment.md new file mode 100644 index 00000000..95cecec4 --- /dev/null +++ b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/ExecutionEnvironment.md @@ -0,0 +1,70 @@ +--- +title: ExecutionEnvironment - Code Documentation +description: An explanation of what ExecutionEnvironment is, its methods, members, and events. +--- + +[*executionEnvironment.js*](TODO) + +ExecutionEnvironment is a class designed to abstract out running the user's code, and also handle the environment itself (such as resetting variables, preloading files, etc). It contains functions to 'compile' user code, run the main program, reset itself, and create directories/files inside the environment. + +The actual implementation can be found inside `executionEnvironment.js`. Upon creation, it creates an iFrame (which can be thought of as a page inside the page) - and this is where all the user's code will be run. +## Why create an iFrame? +The iFrame it creates is sandboxed so that it cannot access anything inside the main page. This is important, since while we can likely trust code the user writes themselves, we cannot trust code they may receive from other people. If we ran the code the user writes directly inside the main page, it could access and manipulate the IDE itself, along with accessing cookies and other things it shouldn't have access to. By running it inside the iFrame, we can be sure it can't access anything it shouldn't. + +It also makes it clear which files are part of the project (as those exist outside the iFrame), and which parts are only transient, such as logs (that only exist inside the iFrame and are destroyed on reloads). User code can not permanently overwrite resources. + +Additionally, it gives us a way to completely reset the environment the code is running in, as we can destroy and recreate the iFrame without having the reload the main page itself. + +To communicate with the iFrame, we can only send and receive messages, which also limits the number of potential escape routes from the iFrame. + +To get an idea of what the ExecutionEnvironment does, here are it's methods, members and events: + +## Members + - `hasRunOnce` - has the program been run yet? Is reset with `resetEnvironment()` + - `executionStatus` - current status of the program, can be: + - `ExecutionStatus.Unstarted` + - `ExecutionStatus.Running` + - `ExecutionStatus.Paused` + +## Methods + - `constructor(container)` - takes a container element to load the iFrame inside. +### Initializing user's code + - `runCodeBlock(block, source)` - takes a code block (which has the block name `block`, and the source code `source`, syntax checks it, and if it passes, sends the code to the iFrame via a message. + - `runCodeBlocks(blocks)` - takes an array of dictionaries with the keys {name, code}, and calls `runCodeBlock` for each one. +### Running user's code + - `runProgram()` - sends a message to the iFrame to run the user's `main` (if it exists). + - `pauseProgram()` - sends a message to pause the user's program - returns a `promise`, that resolves once the program pauses, or fails after 2 seconds. + - `continueProgram()` - sends a message to continue the user's program (if it has been paused) + - `stopProgram()` - sends a message to stop the user's program completely - returns a `promise`, that resolves once the program stops, or fails after 2 seconds. +### Handling the environment + - `resetEnvironment()` - completely resets the environment, by destroying and recreating the iFrame. All files inside the environment will also be lost. + - `cleanEnvironment()` - Does a 'best-efforts' attempt to tidy the environment, such as removing user created global variables. Much faster than `resetEnvironment()`, and does not reset the file system. +### Filesystem + - `mkdir(path)` - sends a message to create a directory + - `writeFile(path, data)` - sends a message to create a directory + +## Events +The events can be listened to by attaching with `addEventListener(event, callback)` + - `initialized` - ExecutionEnvironment is setup and ready to execute code. + - `error` - an error has occurred in user code. +Members: + - `message` - the error message + - `line` - the line number of the error + - `block` - the name of the code block the error occurred in. + - `programStarted` - the program has started running + - `programStopped` - the program has stopped running + - `programPaused` - the program has paused + - `programContinued` - the program has resumed running + - `onMovePath` - A file or directory has been moved. + Members: + - `oldPath` - the original path + - `newPath` - the path it was moved to + - `onMakeDirectory` - A directory has been made. + Members: + - `path` - the path to the new directory + - `onDeletePath` - A file or directory has been deleted. + Members: + - `path` - the path to the file/directory + - `onOpenFile` - A file has been opened, possibly for reading or writing. + Members: + - `path` - the path to the file \ No newline at end of file diff --git a/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/IDBStoredProject.md b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/IDBStoredProject.md new file mode 100644 index 00000000..8c34246f --- /dev/null +++ b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/IDBStoredProject.md @@ -0,0 +1,71 @@ +--- +title: IDBStoredProject - Code Documentation +description: An explanation of what IDBStoredProject is, its methods, members, and events. +--- + +[*IDBStoredProject.js*](TODO) + +IDBStoredProject is a class that handles saving/loading the user's project within the browser itself. It uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB) storage, which allows it to store large amounts of data in a simplified database structure. + +It stores a single project inside a single database, creating a new one for each project. It has functions to read and write files to a virtual filesystem saved inside the database (for storing user code, and uploaded resources like sprites and sounds). It also has an area for config, and keeps track of its lastWriteTime inside there. + +## Database layout +There are two tables: + - `project` - contains information about the project, such as the last write time. Simple key-value, with the key's name being 'category'. + - `files` - stores all the user's files and directories. Each entry contains the following: + - `nodeID` - a numerical identifier for the node (file/directory), automatically increments. + - `name` - name of the file/directory + - `type` - either `"FILE"` or `"DIR"` - file or directory + - `data` - the file's contents - a binary blob of data. Or `null` if it's a directory. + - `parent` - the `nodeID` of the parent of the file/directory (what directory is it inside). -1 means it is inside the root directory. + +## Members + - `initializer` - a function that can be called to initialize the database - performs the equivalent of `skm new` + - `projectName` - the name of the project (and therefore database) it is currently attached to. `null` if detached. + - `lastKnownWriteTime` - the last time the project was written to within this tab. + +## Methods + - `constructor(initializer)` - takes an initializer function, used when initializing a project's database for the first time. + - `attachToProject(storeName)` - attached it to a project with the name `storeName`. Initializes the database, and emits an `attached` event. + - `detachFromProject()` - detaches itself from the project, resets its internal state and emits a `detached` event. + - `deleteProject(storeName)` - deletes the project named `storeName`, and returns a promise which resolves once the database is truly deleted. + - `checkForWriteConflicts()` - checks the `lastKnownWriteTime` against the actual `lastWriteTime` inside the database - if they conflict in a way that suggests another tab has written to the database, throw a `timeConflict` event. + - `access(func)` - a bit of a special function. This function is the only entry point to reading/writing to the IDBStoredProject. It takes a function, which it will call, passing in a new object (internally a `__IDBStoredProjectRW`), which has many more methods for reading/writing. This is done, so that the opening/closing of the database can be wrapped around the user function, without them having to handle it manually (and potentially leave open connections causing issues later on). Here's an example of usage: +```javascript +let storedProject = new StoredProject(...) +... +// we get passed a new object, which we called "project", and can use it to get the lastWriteTime. +// this is all performed asynchronously, so we need to "await" it to get the result +let storedTime = await storedProject.access((project)=>project.getLastWriteTime()); +// in non-lambda syntax +let storedTime = await storedProject.access(function(project){ return project.getLastWriteTime()}); +``` +**The following functions are ones accessible from inside the callback to `access` only** + + - `getLastWriteTime()` - get the last write time. + - `updateLastWriteTime(time = null)` - set the last write time - defaults to the current time (stored in unix time) + - `mkdir(path)` - make a directory at path, does nothing if it already exists. Emits `onMakeDirectory` event. + - `writeFile(path, data)` - overwrites the data inside the file at `path` with `data` - creates the file if it doesn't exist. Emits `onOpenFile` event. Emits `onWriteToFile` event. + - `rename(oldPath, newPath)` - moves a file/directory to a new path and/or name. Emits `onMovePath` event. + - `readFile(path)` - reads a file at `path` and returns the data inside. Returns `null` if the file doesn't exist. + - `getFileTree()` - returns a complete tree of the file system, in a structure digestible by the `TreeView`. + + + +## Events +The events can be listened to by attaching with `addEventListener(event, callback)` + - `attached` - Is attached and can be used. + - `detached` - Has been detached. + - `onMovePath` - A file or directory has been moved. + Members: + - `oldPath` - the original path + - `newPath` - the path it was moved to + - `onMakeDirectory` - A directory has been made. + Members: + - `path` - the path to the new directory + - `onDeletePath` - A file or directory has been deleted. + Members: + - `path` - the path to the file/directory + - `onOpenFile` - A file has been opened, possibly for reading or writing. + Members: + - `path` - the path to the file diff --git a/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/TreeView.md b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/TreeView.md new file mode 100644 index 00000000..1fee015c --- /dev/null +++ b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/classes/TreeView.md @@ -0,0 +1,55 @@ +--- +title: TreeView - Code Documentation +description: An explanation of what TreeView is, its methods, members, and events. +--- + +[*treeview.js*](TODO) + +TreeView is a class used for displaying and updating a tree view, designed specifically around file/directory manipulation. It allows viewing multiple filesystems at once in an overlapping fashion (important since we have the files in the user's project that will be saved/loaded, and also the live files inside the ExecutionEnvironment, which may be different). It allows files/folders to be dragged around and organized, and folders to have a button on the side for uploading new files. + +The way it is intended to be used, is to make it listen to events from the target filesystems (such as file moves/deletes), and update itself accordingly. When it is interacted with by the user, it will emit its own events - these events should be listened to, and the target filesystem updated accordingly. It should then look like this : +1. A file is created in the target filesystem and an event is emitted +2. The TreeView reacts to this event and creates a node in its tree with the same name. +3. The user now drags that node to inside another node (directory), and the TreeView emits an event. +4. A function is called back from this event, that then tells the target filesystem to move the file. +5. The target filesystem moves the file, and an event is emitted. +6. The TreeView reacts to this event, and moves the node to inside the directory. + +See how the TreeView never updates itself - it relies on an event coming _back_ from the target filesystem. This means that if the target filesystem fails to do the operation for whatever reason, the TreeView also remains in the same state, meaning the two remain synchronized effectively. + +See example usage of it inside `fileview.js`, where it is attached to both the `IndexedStorageDB` filesystem, and also the filesystem inside the `ExecutionEnvironment`. + +### Limitations +Currently there is no way to delete files/folders, or rename files/folders in the interface itself. This shouldn't be hard to add, however. + +## Members +None publicly available. + +## Methods + - `constructor(container, FSes)` - takes a container to place the TreeView's elements into, and a list of FSes, which are the filesystems it will support. An example list looks like this `{"persistent":"node-persistent", "transient":"node-transient"}`, key-value pairs where the key is the filesystems name, and the value is a css style to apply to nodes inside this filesystem. + - `moveNode(oldPath, newPath, index = -1, FS)` - moves a node to a new path and/or name. Allows one to set the index the node will appear at, and also which filesystem(s) (a list) the move occurred in. + - `deleteNode(path, FS)` - deletes a node from a set of filesystem(s) (a list) + - `addDirectory(path, FS)` - make a directory at path, does nothing if it already exists. Allows one to set which filesystem(s) (a list) the directory add occurred in. + - `addFile(path, data)` - make a file at path, does nothing if it already exists. Allows one to set which filesystem(s) (a list) the file was added in. + - `reset(path)` - Deletes all nodes. + - `populatefileView(files, FS)` - Populates the tree with a list of files in a particular structure (the same one `IDBStoredProject.getFileTree()` returns). Allows one to set which filesystem(s) (a list) the directory add occurred in. + +## Events +The events can be listened to by attaching with `addEventListener(event, callback)` + - `nodeMoveRequest` - A file or directory has been moved. + Members: + - `treeView` - the TreeView object + - `oldPath` - the original path + - `newPath` - the path it was moved to + - `FS` - the filesystem(s) the change occurred in. + - `accept` - a function that can be called to announce that the change was successful - **currently unused**. + - `folderUploadRequest` - The 'add file' button was clicked on on a directory. + Members: + - `treeView` - the TreeView object + - `path` - path to the directory + - `FS` - the filesystem(s) the directory exists in. + - `nodeDoubleClick` - A file node has been double clicked. + Members: + - `treeView` - the TreeView object + - `path` - path to the file + - `FS` - the filesystem(s) the directory exists in. \ No newline at end of file diff --git a/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/processes/HowSplashKitOnlineRunsCode.md b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/processes/HowSplashKitOnlineRunsCode.md new file mode 100644 index 00000000..5cd14cae --- /dev/null +++ b/src/content/docs/products/splashkit/documentation/splashkit-online/code-documentation/processes/HowSplashKitOnlineRunsCode.md @@ -0,0 +1,538 @@ +--- +title: How SplashKit Online runs the user's code! +description: A detailed explanation as to all the steps SplashKit Online takes to execute the user's code. +--- + +## Introduction +This document is a deep dive into how SplashKit Online runs the user's code. This is a multi-step process that will take us through much of SplashKit Online's code, so get ready! +## Overview +Here's a _very_ brief overview of how it works. Don't worry if you don't understand what this means yet! Each part will be explained in due time - but feel free to use this as a reference of the overall process. +1. Before the user does anything... + 1. The IDE starts up, and creates an ExecutionEnvironment. + 2. The ExecutionEnvironment creates an iFrame, and loads SplashKit inside it. +2. User writes code into the code editor (currently there are two 'code blocks', General and Main). +3. User presses the Run button. + First we have to run the code blocks, to create all the user's functions/classes and initialize global variables. +4. Pressing run calls`ExecutionEnvironment.runCodeBlocks`, passing in the General Code and Main Code code blocks. +For each code block: + 1. The code block's text is sent as an argument to `ExecutionEnvironment.runCodeBlock(block, source)` + 2. The source code gets syntax checked. + 3. If it is syntactically correct, it is then sent as a `message` into the ExecutionEnvironment's iFrame. +6. The following steps all happen inside the iFrame (for security purposes) + 1. The iFrame receives the message. + 2. The code is transformed to make it runnable within the environment + 3. A real function is created from the transformed code. + 4. **The code is run!** +7. Now it needs to run the user's main: +`ExecutionEnvironment.runProgram()` is called. +8. This sends a message into the iFrame. +9. The following steps all happen inside the iFrame (for security purposes) + 1. The iFrame check if the user has created a `main()` + 2. **If so, `main()` is run!** + + +:::note +If you're wondering why the user's 'code blocks' get run, and only _then_ the user's main program gets run, here's why. +Javascript is a completely dynamic language, so unlike compiled languages like C++, functions and classes and so on aren't +known ahead of time. Instead, the creation of a function/class itself is runtime code. The code +```javascript +function myFunction(){ + return 4; +} +myFunction(); +``` +is _run_, to create a function called `myFunction`, that can now be called later on. + +In a similar way, functions themselves are just objects, and can be assigned as follows: +```javascript +let myFunction = function(){ + return 4; +} +myFunction(); +``` + +When we first run the user's code blocks, we are creating all their functions and classes and global variables. + +Only after this is done, can we then call `main()`, and start the program itself. But as you know now, in a way it was running the whole time. +::: + +## Before the user does anything... +Looking inside `script.js` +```javascript +// ------ Setup Project and Execution Environment ------ +let executionEnviroment = new ExecutionEnvironment(document.getElementById("ExecutionEnvironment")); +``` +First, an `ExecutionEnvironment` is created. + +From the [Source Code Documentation](../../classes/executionenvironment/) +> ExecutionEnvironment is a class designed to abstract out running the user's code, and also handle the environment itself (such as resetting variables, preloading files, etc). It contains functions to 'compile' user code, run the main program, reset itself, and create directories/files inside the environment. + +When created, an important thing it does is create an iFrame (sort of a page inside the page), which is where all code execution will take place. This is done for security, see [here](TODO) for a more detailed explanation. + +Inside the iFrame, the page `executionEnvironment.html` is loaded, which loads in things like the SplashKit library itself, and also the executionEnvironment internal scripts, like `executionEnvironment_Internal.js` and `executionEnvironment_CodeProcessor.js` + +Once the environment finishes loading, it sends out an `initialized` event - this is when all the green buttons in the interface become usable, and code can be executed! +## User writes their code, then presses run +Pressing the run button does three things: +```javascript +clearErrorLines(); + +runAllCodeBlocks(); +/* This is what it looks like inside "runAllCodeBlocks": +executionEnviroment.runCodeBlocks([ + {name: "GeneralCode", code: editorInit.getValue()}, + {name: "MainCode", code: editorMainLoop.getValue()} +]); +*/ + +executionEnviroment.runProgram(); +``` +*from [script.js - runProgram()](TODO)* +1. First it clears the error lines from the code editors. +2. Next, it calls `executionEnviroment.runCodeBlocks`, and gives it the two code blocks and the source code inside the code editors; this runs the user's code, which really means runs all the function/variable/class initialization. +3. Finally it runs the program - this runs the user's `main` function. +Let's look at step 2 more closely. + +## Pressing run calls `ExecutionEnvironment.runCodeBlocks`, passing in the General Code and Main Code code blocks. + +We can see by looking at the source code, that `runCodeBlocks` just calls `runCodeBlock` for each block passed in. +```javascript +runCodeBlocks(blocks){ + for (let block of blocks){ + this.runCodeBlock(block.name, block.code); + } +} +``` +*from [executionEnvironment.js](TODO)* + +So let's have a look at `runCodeBlock` +```javascript +runCodeBlock(block, source){ + // Syntax check code - will throw if fails. + this._syntaxCheckCode(block, source); + + this.iFrame.contentWindow.postMessage({ + type: "RunCodeBlock", + name: block, + code: source, + }, "*"); +} +``` +*from [executionEnvironment.js](TODO)* + +First thing it does is call the internal function `_syntaxCheckCode(block, source)`, which as the name says, will syntax check the code. The way this syntax checking works is somewhat complicated, but let's step through it. + +### Some backstory (optional reading) +Just as a precursor, in Javascript there are multiple ways to execute code that the user provides as text. One way is to use the function `eval`, for example you can run +```javascript +eval("alert('Hello!');"); +``` +and this will pop up a box, as if you had directly run +```javascript +alert('Hello!'); +``` +This method combines syntax checking and running together - first the browser syntax checks the code, and then it runs it. However, we want to syntax check the code _before_ running it. The main way to do this, is to create a _`Function` object_ from the source code. The browser will syntax check the code when making it, without running it yet. As will be explained later, it turns out we actually _need_ to make a `Function` object anyway, for certain important features like pausing the code and allowing while loops. + +This can be as simple as +```javascript +let myFunction = new Function("alert('Hello!');"); +``` +However, we also need to be notified of any errors that occur, so we can tell the user about them. If you are familiar with Javascript, you might suggest a `try/catch` block, like this: +```javascript +try{ + let myFunction = new Function("alert('Hello!');"); +} +catch(error){ + // tell the user about the error +} +``` +It 'tries' to create the new function, and if it fails, we catch the error. It turns out we can get the error message and line number from that `error`, so this seems like it will work. The problem with this, is that the actual 'error' that occurred, technically occurred on the line where `new Function(...)` was called, and not the line inside the user's code, meaning the line number we get back is useless. So instead the method described next is what was used. + +### Syntax Checking +The method used for syntax checking is to create a `Function` object from the user's source code, which let's us do the syntax check without running the code. For reasons that will be explained later, we actually create an `AsyncFunction`, which will let us run the code in a more flexible way later on. + +To retrieve any syntax error's that might occur when checking, we listen to the main window's `error` event, which reports any errors that happen, and where they happened. + +So the code to perform the syntax check looks a bit like this: +``` +Attach to the "error" event + If the event gets called next, report the error to the user. + +Create the function - if the syntax check fails, the "error" event will get called, and the function will fail here. + +Detach from the "error event" +``` + +One important aspect of implementing this, is that inside the sandboxed iFrame, the information we get in the `error` event is very unhelpful - the line number is always 0, and the error message is very generic. Luckily, since we are just syntax checking (and not _running_) the code, we can just do the syntax check inside the main page instead of the iFrame - so this is what happens. + + +Once the code passes syntax checking, it is sent into the iFrame for the next steps. Let's have a look at the code running inside the iFrame that receives the code: +```javascript +if (m.data.type == "RunCodeBlock"){ + let processedCode = ""; + try { + processedCode = processCodeForExecutionEnvironment(m.data.code, "mainLoopStop", "mainLoopPause", "mainLoopContinuer", "onProgramPause"); + + tryEvalSource(m.data.name, processedCode); + } + catch(e) { + ReportError(userCodeBlockIdentifier+m.data.name, "Unknown syntax error.", null); + } +} +``` + +Let's break this down. First, it tries to run `processCodeForExecutionEnvironment`, passing in the user's code and some other parameters. We'll see what that does in a moment, but for now know that it takes the user's code, and _changes it_, to allow us to pause it, resume it, reset it, etc. Assuming it's successful, then we move to `tryEvalSource`, which makes a new `AsyncFunction` from this modified source code, and then runs it! Remember, these stages all take place securely inside the iFrame. + +Let's look at how the code modification/transformation works, and why we do it. + +## The code is transformed to make it runnable within the environment +### Code Transformation + +#### Why do we modify/transform the user's code? +There are a couple of things that we want the user's code to be able to do, that's impossible to support without modifying their code. +##### We want them to be able to have infinite while loops +It's pretty normal to have code that looks like this in a C program: +```c +void main(){ + bool quit = false; + while(!quit){ + ...do stuff... + } +} +``` +where it just loops and loops until the user quits. However, in a browser, JavaScript is executed on the same thread as the page. So normally the browser might do something like this: +1. Check for user input +2. Update the page +3. If the user clicks the button, **run some JavaScript** +4. Goto 1 + +Which works fine if the 'run some JavaScript' part ends quickly. But if it enters a loop, like in the code above, then the browser won't be able to check for input or even update the page until the code ends - if it's an infinite loop like above, the page can only crash. + +What's the solution? We modify loops inside the user's code, so that they give control _back_ to the browser periodically. This is done with JavaScript's `async` function support, and requires all user functions to be marked as `async`, to have calls to those functions marked with `await`, to have code inserted in every loop to handle the control passing, and to have user classes have some changes (since constructors can't be async). + +Here are some more specific details (optional reading): + - All loops automatically await a timeout of 0 seconds after executing for more than ~25ms. + - screen_refresh (and other similar functions) await a window.requestAnimationFrame + - All user functions are marked as async, so that they can use await. + - Similarly, all calls to user functions are marked with await. + - Constructors cannot be async, so rename all constructors of user classes to `__constructor`, and call it when user classes are newed. `let player = new Player()` becomes `let player = (new Player()).__constructor()` + +This same setup is used to enable code pausing, and stopping, by simply listening for *pause/stop/continue **flags*** +when it does the awaits. To stop, we simply throw a 'ForceBreakLoop' error. To pause, we create +a promise and await it. To continue, we call that promise. + +*Here's something important to note, for those wondering why we just don't use `eval` instead of putting the user's code in a new `Function` object. We couldn't do this transformation if we didn't put the user's code inside a function, because you cannot `eval` asynchronous code! Meaning the user couldn't write while loops, or any long running code at all!* + +##### We want the user to be able to declare global functions, variables, and classes in one block and be able to access them in another. +When we evaluate the user's code, we are technically sticking it inside a function, then running it. As such, the variables, functions and classes declared are actually scoped to that function, meaning they vanish once the function ends. This obviously isn't very helpful - the user couldn't define things in one code block, and use them in another, because they're in different scopes! In fact, we couldn't even run the user's main, since it would vanish just after the code that creates it finishes evaluating. + +We could just combine the user's code together into a single piece that executes in the same scope, but then we couldn't have hot-reloading, where the user can update their code _while_ the program runs. + +So what we do, is modify the user's code, so that declarations made inside the "global" scope, are manually assigned to the _real_ global scope outside the function that the user's code is written in. Just as an example, imagine the user has written the following code. + +General Code: +```javascript +let globalVariable = "Hello!"; +``` +Main: +```javascript +function main(){ + write_line(globalVariable); +} +``` +If we evaluated each block by putting the block's code directly into a new `Function` and running the function, it would be equivalent to the following: + +```javascript +function GeneralCode(){ + let globalVariable = "Hello!"; +} +function MainCode(){ + function main(){ + write_line(globalVariable); + } +} + +// Init the user's functions, variables, etc +GeneralCode(); +MainCode(); + +// Start the program! +main(); +``` +Hopefully it's clear why this wouldn't work. + +Here's how we transform it: +```javascript +function GeneralCode(){ + window.globalVariable = "Hello!"; +} +function MainCode(){ + window.main = function main(){ + write_line(globalVariable); + } +} + +// Init the user's functions, variables, etc +GeneralCode(); +MainCode(); + +// Start the program! +main(); +``` + +Notice how every time we define something that should be in the global scope, we assign it to `window`? This is (*one name for*) the global scope in JavaScript. So now the variables/functions/classes are actually in the global scope, and everything works as expected. + +##### We also want them to be able to restart their program without old variables and functions being left behind. + +Now that we have the variables in the global scope, we have a problem. Let's say the user runs the program above once. They then remove the line of code defining `globalVariable`. If they restart their program, you'd expect that an error occurs when they reach the line `write_line(globalVariable);`, since `globalVariable` isn't defined right? + +But no error occurs! This is because, the global variable was already set the _first_ time they ran the program, and when they 'restarted' it, all we did was call `main()` again, meaning the global variable stayed in existence! We could fully reset the executionEnvironment with `resetEnvironment()`, but this takes a long time (up to 20 seconds), so doing this every time the user runs their code would be a poor user experience. + +Luckily, we already know what the global variables are - we already transform them after all. So what we can do is keep a list of them, and then when the user restarts the program, we can `delete` all the variables from the global `window` object, and then we get a clean run; hence the function `cleanEnvironment()` exists. Now when the user runs, they'll get an error as they should! + +#### How do we modify the code? +While we could just modify the text as a string, this is error prone and kind of hacky. Instead, we use a Javascript library called **Babel**, which parses the user's Javascript, and creates what's called an AST (or abstract-syntax-tree), which lets us treat each part of the code as separate objects we can manipulate. For example: + +```javascript +let a = 10; +``` + +might become something like + +```javascript +VariableDeclaration(Identifier('a'), NumericLiteral(10), type='let') +``` + +There's no need to understand this too deeply, but it's just good to know. + +#### Putting it all together +Now we have all the pieces needed to understand the `processCodeForExecutionEnvironment` function. +```javascript +function processCodeForExecutionEnvironment(userCode, asyncStopName, asyncPauseName, asyncContinueName, asyncOnPauseName){ + + asyncifyTransform__asyncStopName = asyncStopName; + asyncifyTransform__asyncPauseName = asyncPauseName; + asyncifyTransform__asyncContinueName = asyncContinueName; + asyncifyTransform__asyncOnPauseName = asyncOnPauseName; + + // Find the user's global declarations - important for next step + // Couldn't find a way to return extra information, so they are stored + // in the global 'findGlobalDeclarationsTransform__userScope' + Babel.transform(userCode, { + plugins: ["findGlobalDeclarationsTransform"] + }); + + // Now do the actual transforms! + userCode = Babel.transform(userCode, { + plugins: ["makeFunctionsAsyncAwaitTransform","asyncify"], + retainLines: true + }); + + return userCode.code; +} +``` + +We can see it takes the user's code, and also some _names_ for the variables that will handle making the code stop/pause/continue - these are the _flags_ mentioned earlier. It also takes the name of a callback to call, when the user's code actually pauses. + +We can see the first thing it does is assign these to some variables - you can ignore that part for now, it's just an implementation detail (it doesn't seem possible to pass parameters into Babel transforms, so I just used global variables...). But after that, it calls Babel with the `"findGlobalDeclarationsTransform"`, this handles updating the list of global variables that we clear when restarting the program. Then we run it again with two more passes - `"makeFunctionsAsyncAwaitTransform"`, and `"asyncify"`, which handle making functions/calls async/await along with the scope changes, and inserting the yielding back to the browser during loops, respectively. + + +## A real function is created from the transformed code. +```javascript +processedCode = processCodeForExecutionEnvironment(m.data.code, "mainLoopStop", "mainLoopPause", "mainLoopContinuer", "onProgramPause"); + +tryEvalSource(m.data.name, processedCode); +``` +Hopefully we now understand what the first line here does. Now we get to actually run the processed code! First we have to turn it into a real function, and this is exactly what `tryEvalSource` does first. Let's have a look inside: +```javascript +async function tryEvalSource(block, source){ + // First create and syntax check the function + let blockFunction = await createEvalFunctionAndSyntaxCheck(block, source); + + if (blockFunction.state != "success") + return blockFunction; + + return await tryRunFunction( + blockFunction.value, + reportError + ); +} +``` +As can be seen, the first thing that happens is that we call `createEvalFunctionAndSyntaxCheck`, which does exactly what it says. You'll notice we're syntax checking here as well - this isn't exactly deliberate, it just happens automatically when the `Function` object is created. Still, it's helpful if the Babel output had a syntax error, for instance. +The important part is inside `createEvalFunctionAndSyntaxCheck`, here: +```javascript +return Object.getPrototypeOf(async function() {}).constructor( + "\"use strict\";"+source+"\n//# sourceURL="+userCodeBlockIdentifier+block +); +``` +Here's where the user's code _finally_ becomes a real function, that will actually be called! Notice it looks a little different to the `new Function("...")` example earlier. This is because, it's creating an `AsyncFunction`, which doesn't have a nice constructor, so we access it directly. The `AsyncFunction` is important, because all of that work we did before modifying the user's code to give control back to the browser when it loops, won't work without it being an `AsyncFunction`! + +You'll also notice that we modify the user's code slightly; we don't just pass `source` directly, we add `"use strict";` at the start, and `//# sourceURL=...` at the end. What do these do? + - `"use strict;"` makes the user's Javascript code execute in strict mode, which tidies up a lot of the language's semantics, forces variable declarations to be explicit, and overall improves code quality and makes errors easier to track down. We couldn't turn on `"use strict";` without the manual scoping fixes either! + - `//# sourceURL=...` tells the browser what 'source file' the code is from. This means that when the browser reports an error, we'll be able to tell what code block it came from! Notice we add `userCodeBlockIdentifier` at the start? This is just a short string that we can use to help us tell if an error came from user code, or if it came from code in the IDE itself. An example might look like this `//# sourceURL=__USERCODE__MainCode`, and so if an error occurs, we will see it came from `__USERCODE__MainCode`, and tell the user it came from their "Main Code" block! + +Now we can finally call this function to run the user's code! Remember, this won't run their _program_ but it will run the code which creates all their functions, global variables, classes, and of course their `main()` function. Actually running the code happens inside `tryRunFunction`, and we'll look at that in just a short bit. But just know now that the code has been run (or failed with an error); let's assume it successfully ran, and so we can actually run the user's `main`! + + +## Now it needs to run the user's main +If we recall, this all started with the user pressing the Run button, which looked like this: +```javascript +clearErrorLines(); + +runAllCodeBlocks(); +/* This is what it looks like inside "runAllCodeBlocks": +executionEnviroment.runCodeBlocks([ + {name: "GeneralCode", code: editorInit.getValue()}, + {name: "MainCode", code: editorMainLoop.getValue()} +]); +*/ + +executionEnviroment.runProgram(); +``` +*from [script.js - runProgram()](TODO)* +We now know what `runAllCodeBlocks` does quite well - it syntax checks the code, sends it to the iFrame, the code gets transformed, stuffed into a function, and then run! So what does `executionEnviroment.runProgram()` do? It's comparatively _much_ simpler! + +First thing it does is send a message to the iFrame, telling it to run the program - we definitely don't want to run the program in the main page, so this is all secured inside the iFrame, like the execution earlier. Upon receiving this message, it then calls its own internal `runProgram()` + +```javascript +async function runProgram(){ + if (window.main === undefined || !(window.main instanceof Function)){ + ReportError(userCodeBlockIdentifier+"Program", "There is no main() function to run!", null); + return; + } + if (!mainIsRunning){ + mainLoopStop = false; + + mainIsRunning = true; + parent.postMessage({type:"programStarted"},"*"); + await tryRunFunction(window.main); + mainIsRunning = false; + parent.postMessage({type:"programStopped"},"*"); + } +} +``` + +Let's break this down. + +First, it checks to see if the main program even exists: +```javascript +if (window.main === undefined || !(window.main instanceof Function)) +``` +We can see how it's just checking the 'global' scope of `window` - which is the same one we know the user's functions get assigned to! So if the use created a `main` function, we'll be able to find it. We also make sure it _is_ actually a function, and that they didn't do something like `let main = 10;` + +Next we make sure it isn't already running. If it was, we could end up with `main()` running multiple times simultaneously, not ideal! +```javascript +if (!mainIsRunning){ + mainLoopStop = false; +``` + +If it wasn't already running, it's time to start it! First turn off the `mainLoopStop` flag. Remember the async control flags mentioned earlier - this is one of them! If it's `true`, the program will stop as soon as it can, so we make sure it's `false`. + +```javascript + mainIsRunning = true; + parent.postMessage({type:"programStarted"},"*"); + await tryRunFunction(window.main); + mainIsRunning = false; + parent.postMessage({type:"programStopped"},"*"); +``` +Now we set `mainIsRunning` to `true` (so that we can't start it multiple times at the same time), and post a message to the outside window `"programStarted"` - there's a listener in the main page that will then change the green buttons accordingly. + +Finally, the moment of truth: `await tryRunFunction(window.main);` +We run the program! It's called with `await`, which means that the code will _wait_ for it to finish before continuing. Remember we made all the user functions `async`? This allows them to give control back to the browser momentarily, but it also means that they can't stop things that call them from continuing to the next line of code - so we `await` to make sure we wait for the program to completely stop. + +Once it does finally end (which will happen if we set `mainLoopStop` to `true`), we set `mainIsRunning` back to `false`, so the user can start it again, and then post a message back to the main window `"programStopped"`, which will again update the buttons accordingly. + +### `tryRunFunction(func)` - what does it do? +The responsibility of `tryRunFunction` - which is used when running the code blocks earlier as well, is to run the user's code, and then detect when it has errors and report them to the user. + +These aren't syntax errors in this case, these are runtime errors (for instance if the user tries to call a function that doesn't exist, or access outside the bounds of an array), and so we go about detecting them in a way a bit different to the syntax errors before. + +And after all, we can't use the window's `error` callback for the same reasons mentioned earlier - inside the iFrame, the error message is generic, and line number reported is always 0! And we certainly can't run the code outside the iFrame, or that would defeat the entire point of having it. + +If we look inside `tryRunFunction`, we'll see it actually ends up calling `tryRunFunction_Internal`, which is a bit more interesting. Here's a simplified version: +```javascript +async function tryRunFunction_Internal(func) { + try{ + await func(); + return "success!" + } + catch(err){ + if (err instanceof ForceBreakLoop){ + return "Stopped" + } + + let error = parseErrorStack(err); + return error; + } +} +``` +We can see it takes the user's function (for instance, the user's `main()`, or the `AsyncFunctions` we made from their code blocks), and tries to run it. It waits for it to finish with `await`, and if it finishes without issues, it returns "success!". + +However, if an error was thrown, we catch it. If it was a `ForceBreakLoop` error, then we know it threw it because the user pressed the Stop button, not because it crashed, and so we just report back that it "Stopped". However, if that didn't happen, we figure out information about the error (such as its line number and what code block it happened in) with `parseErrorStack(err)`, and then return information about the error. + +This information is received by the original `tryRunFunction`, and if an error occurred it reports it to the user via `ReportError`. + +Let's take a closer look at `parseErrorStack`, as the last stop on our journey. +### parseErrorStack +Once we catch an error, the problem becomes "how do we report it to the user?" We need to give them the error message, and at least a line number and code block to look at. If the error message had members like `err.lineNumber` or `err.fileName` it'd be great, but they don't (unless you're using Firefox...). However, all modern browsers support `err.stack`, which gives us a piece of text describing the error and where it happened. It looks a bit like this: +```javascript +gameInnerLoop@Init.js;:25:25 +main@Main.js;:25:11 +async*tryRunFunction_Internal@http://localhost:8000/executionEnvironment_Internal.js:57:21 +tryRunFunction@http://localhost:8000/executionEnvironment_Internal.js:89:21 +runProgram@http://localhost:8000/executionEnvironment_Internal.js:132:15 +@http://localhost:8000/executionEnvironment_Internal.js:167:9 +EventListener.handleEvent*@http://localhost:8000/executionEnvironment_Internal.js:144:8 +``` + +We can see on each line, the function, filename, line number, and even column number! The problem, is that `stack` is actually non-standardized JavaScript, and so each browser implements it slightly differently. Additionally, we still have to actually parse (read) the string, to get all the information out of it. This is the job that `parseErrorStack` performs. + +The actual method isn't that complicated. It uses a regex that is designed to work across both Firefox and Chrome based browsers (including Edge), that reads out the file name and line number. It then returns these! Not too hard overall. +One thing to note, is there are two lines inside `parseErrorStack` that might be confusing: +```javascript + if (file.startsWith(userCodeBlockIdentifier)) + lineNumber -= userCodeStartLineOffset; +``` +Once we have extracted the line number, we check to see if the file name starts with the `userCodeBlockIdentifier` (remember this from earlier, when we added the `//# sourceURL=` to the user's code to help identify it?). If it starts with this, we know it's user code. And then we subtract `userCodeStartLineOffset` from it. Why do we do that? The answer is that when we create the `AsyncFunction` object, Firefox actually adds some lines to the start. For example, let's say we create a simple function from text: +```javascript +let myFunc = new Function("console.log('Hi!');"); +``` +If we were to look at the function's source code with: +```javascript +myFunc.toString() +``` + +We get the following (at least in Firefox): + +```javascript +function anonymous( +) { +console.log('Hi!'); +} +``` +See how there are two extra lines at the start? When the ExecutionEnvironment starts, it actually detects how many lines the browser adds at the start, and stores it inside `userCodeStartLineOffset` - so in Firefox, `userCodeStartLineOffset` is equal to `2`. Subtracting this from `lineNumber` then gives us the _actual_ line number of the error, so that we can highlight it in the user's code editor. + +## Recap +Hopefully having read all of that, you have a decent understanding of the steps SplashKit Online takes to run the user's code! As a recap, let's have one more look at the overview, which hopefully makes a lot more sense now. +1. Before the user does anything... + 1. The IDE starts up, and creates an ExecutionEnvironment. + 2. The ExecutionEnvironment creates an iFrame, and loads SplashKit inside it. +2. User writes code into the code editor (currently there are two 'code blocks', General and Main). +3. User presses the Run button. +First we have to run the code blocks, to create all the user's functions/classes and initialize global variables. +4. Pressing run calls`ExecutionEnvironment.runCodeBlocks`, passing in the General Code and Main Code code blocks. +For each code block: + 1. The code block's text is sent as an argument to `ExecutionEnvironment.runCodeBlock(block, source)` + 2. The source code gets syntax checked. + 3. If it is syntactically correct, it is then sent as a `message` into the ExecutionEnvironment's iFrame. +6. The following steps all happen inside the iFrame (for security purposes) + 1. The iFrame receives the message. + 2. The code is transformed to make it runnable within the environment + 3. A real function is created from the transformed code. + 4. **The code is run!** +7. Now it needs to run the user's main: +`ExecutionEnvironment.runProgram()` is called. +8. This sends a message into the iFrame. +9. The following steps all happen inside the iFrame (for security purposes) + 1. The iFrame check if the user has created a `main()` + 2. **If so, `main()` is run!**