Skip to content

HyperQuark/vm

Repository files navigation

experimental wasm compiler for scratch, based on turbowarp.

TurboWarp/scratch-vm

Modified Scratch VM with a JIT compiler and more features.

The public API of TurboWarp/scratch-vm should be compatible with LLK/scratch-vm. See "Public API" section below for more information.

Setup

See https://github.com/TurboWarp/scratch-gui/wiki/Getting-Started to setup the complete TurboWarp environment.

If you just want to play with the VM then it's the same process as upstream scratch-vm.

Public API

Any public-facing API in LLK/scratch-vm should work just fine in TurboWarp/scratch-vm. Anything that doesn't is a bug. TurboWarp adds some new methods to the public API.

Runtime.setFramerate / VirtualMachine.setFramerate

setCompatibilityMode is deprecated (but still works) in favor of a generic setFramerate method.

runtime.setFramerate(60);

There is an event for framerate changes on Runtime and VirtualMachine: FRAMERATE_CHANGED (emitted with new framerate as only argument)

Runtime.setInterpolation / VirtualMachine.setInterpolation

Toggles frame interpolation, an experimental feature that tries to make project motion smoother without changing the script tick rate.

There is an event for changes on Runtime and VirtualMachine: INTERPOLATION_CHANGED

Runtime.setCompilerOptions / VirtualMachine.setCompilerOptions

This lets you change the behavior of the compiler. This method takes an object with the following arguments:

  • enabled (boolean; default true) - controls whether the JIT compiler is enabled
  • warpTimer (boolean; default false) - controls whether to use a warp timer to limit how long warp scripts can run. Can have significant performance impact
runtime.setCompilerOptions({
  enabled: true,
  warpTimer: true
});
// Partial updates are also supported -- this will only change `enabled` and not any other properties
runtime.setCompilerOptions({ enabled: false });

There is an event for compiler option changes on Runtime and VirtualMachine: COMPILER_OPTIONS_CHANGED (called with current options)

Runtime.setRuntimeOptions / VirtualMachine.setRuntimeOptions

Similar to setCompilerOption. This lets you control some behavior of the runtime.

  • maxClones (number; default 300) - controls the clone limit; Infinity to disable
  • miscLimits (boolean; default true) - controls various limits such as pitch, pan, etc.
  • fencing (number; default true) - controls whether sprite fencing should be enabled

There is an event for runtime option changes on Runtime and VirtualMachine: RUNTIME_OPTIONS_CHANGED (called with current options)

Runtime.stop / VirtualMachine.stop

Stops the tick loop. This does not touch the active thread list. Anything currently active will be resumed when start is called again.

There is an event for stop on Runtime and VirtualMachine: RUNTIME_STOPPED (similar to RUNTIME_STARTED)

Runtime.stageWidth / Runtime.stageHeight

These control the width and height of the stage. Set them to values other than 480 and 360 respectively to get custom stage sizes. Keep in mind that you need to manually resize the renderer as well.

COMPILE_ERROR event

A COMPILE_ERROR is fired on Runtime and VirtualMachine when a script couldn't be compiled.

Extension authors

If you only use the standard reporter, boolean, and command block types, everything should just work without any changes.

Compiler Overview

The source code for the compiler is in src/compiler. Script compilation happens "Just In Time" when a thread is started.

I'm going to try to explain some of the high-level details of the compiler below. If you have any questions, just ask. Open an issue or something, I'm very easy to get ahold of.

Intermediate representation

Source: src/compiler/irgen.js

The first stage of the compiler is to generate an intermediate representation (IR). This is really just a more abstract version of Scratch's AST. Some analysis and optimizations happen at this stage.

In the future there may also be multiple different code generators, and having this abstraction will be even more useful then.

JavaScript generation

Source: src/compiler/jsgen.js

The IR is passed into the JavaScript generator which descends the IR and generates optimized JavaScript. This JavaScript has some idiosyncrasies. I'll try to explain some of them using a simple project with a bouncing cat (https://scratch.mit.edu/projects/437419376) as an example.

This is an example result of compiling a project.

function factory0 (target) {
  // Note: manually formatted to be more readable. The actual code has no indentation or other formatting.
  const runtime = target.runtime;
  const stage = runtime.getTargetForStage();

  const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];

  return function* gen0 () {
    b0.value = 0;
    target.setXY(0, 0);

    for (var a0 = 100; a0 >= 0.5; a0--) {
      b0.value = ((+b0.value || 0) + 1);
      runtime.ext_scratch3_motion._moveSteps(b0.value, target);
      runtime.ext_scratch3_motion._ifOnEdgeBounce(target);

      if (thread.warp === 0) yield;
    }

    retire();
  };
}

There's a lot to unpack here, so let's start from the top.

Factory function

The factory function is the function that the JavaScript generator returns. This is run once when a thread is started. This function takes the current target as an argument, sets up any required variables, and returns another function (keep reading). There is also an implied thread variable that represents the current thread.

Factory variables

The factory function sets up various pieces of data that the script needs to run. For example, every script gets access to the runtime and stage objects. These variables will be shared amongst all instances of the generator function returned by the factory function (described below).

Generator function

The factory function returns a generator function. When a script is run, this is the code that runs. If a procedure runs 20 times, this script runs 20 times. If a procedure never runs, this function is never run.

This function is usually a generator function. The interesting thing about generator functions is that they can yield at arbitrary points and resume later, which is necessary to support control flow used by Scratch.

The actual script

Let's break this down line-by-line.

b0.value = 0;

Sets the value property of b0 (a factory variable) to 0. This is where variable values are stored.

target.setXY(0, 0)

Sets the target's position to (0, 0).

for (var a0 = 100; a0 >= 0.5; a0--) {

This is really just a regular for loop that will repeat 100 times. The loop index, a0, is called a local variable. These are variables that are local to each invocation of the generator function. In this case, it's being used as a loop index to iterate 100 times.

b0.value = ((+b0.value || 0) + 1);

Changes b0.value by 1. (+b0.value || 0) converts the value of b0.value to a number (in Scratch, NaN becomes 0). This is necessary because the compiler can't statically determine that b0.value will always evaluate to a number. Why can't it, even when b0.value was just set to the number 0 earlier? Because the script may have yielded control and another script may have changed the type of the variable, as you'll see later. If the variable changes to the string "10" and the script does not perform this conversion, then you would get "10" + 1 which evaluates to the string "101", not the number 11.

runtime.ext_scratch3_motion._moveSteps(b0.value, target);

Accesses the scratch3_motion extension through the runtime factory variable and calls _moveSteps with b0.value (distance to move) and target (the target to operate on).

runtime.ext_scratch3_motion._ifOnEdgeBounce(target);

Similar to the above, it runs the "if on edge, bounce" block on the current target.

if (thread.warp === 0) yield;

This is what makes JavaScript generator functions powerful: they can yield and resume later. This particular expression says "if not running in warp mode (run without screen refresh), then yield and let another script run."

retire();

This runs the runtime function retire. This function marks the current thread as finished so that it will not run again. There are many other runtime functions for lots of operations such as list replacements, insertions, gets, etc. These are defined in src/compiler/jsexecute.js

Procedures

One factory function exists for each procedure used by a thread. These work the same as described above: factory function runs once, generator functions run as needed. The only difference is that generator functions for procedures accepts arguments and can be called.

Compatibility layer

Not every block needs to be compiled to optimized JavaScript. For example, will anyone notice if the "play sound until done" block runs slightly slower than it could? Probably not. Would people notice if there was a subtle bug that broke 1 in 10 sounds? Definitely. This is why the compiler also has a "compatibility layer" that allows it to use the Scratch implementation of blocks rather than writing a second "compiled" version.

For example, an addition in TurboWarp is compiled down to the JavaScript + operator: firstThing + secondThing. If the + operator instead ran in the compatibility layer, it would look like: yield* executeInCompatibilityLayer({"OPERAND1":firstThing,"OPERAND2":secondThing}, runtime.getOpcodeFunction("operator_add")) and the actual addition would happen in Scratch's definition of the operator_add block.

The opcodes that this is used for are in src/engine/compat-blocks.js

Errors

When scripts can't be compiled, they run in the standard (slow) scratch-vm interpreter. This is used for some edge cases: monitor threads, unknown opcodes, edge-activated hats, other errors.

License

TurboWarp's modifications to Scratch are licensed under the GNU Lesser General Public License version 3. See COPYING and COPYING.LESSER for more information.

Original license for scratch-vm:

Copyright (c) 2016, Massachusetts Institute of Technology
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

About

No description, website, or topics provided.

Resources

License

LGPL-3.0, GPL-3.0 licenses found

Licenses found

LGPL-3.0
COPYING.LESSER
GPL-3.0
COPYING

Stars

Watchers

Forks

Packages

No packages published

Languages