Skip to content

homebots/vm

Repository files navigation

Espresso

Espresso is a minimal JavaScript-like language designed for a lightweight VM written in C. It focuses on predictability, ease of compilation, and simplicity for hardware-oriented use cases such as microcontrollers.


Goals

  • Small, deterministic syntax and semantics.
  • Easy compilation to a compact bytecode format.
  • No hidden execution at module load time.
  • Hardware-friendly: predictable control flow, low runtime overhead.
  • Support for intrinsic modules and system calls through the same function call mechanism as user-defined functions.

Language Subset

Espresso uses familiar JavaScript syntax but with significant restrictions.

1. Modules

  • Everything is a module — no global namespace.
  • Two kinds of modules:
    1. Libraries
      • Top-level: only import, export, function and const declarations.
      • No main() requirement.
    2. Entry point modules
      • Top-level: only import, export, function and const declarations.
      • Must have a main function as the program entry point.
      • No top-level function calls — execution always starts at main().

2. Imports & Exports

  • Allowed:
import { foo, bar } from 'lib';
export function fn() { ... }
export const foo = 1;
  • Not allowed:
    • Import and export aliasing (as).
    • Default exports.
    • Named exports (export { foo, bar })
    • export with let

3. Variables & Constants

  • let and const are supported.
  • Variables are function-scoped or module-scoped — no block scope.
  • var is not supported.
  • No closures — a function cannot reference variables from another function except via parameters. But a function can reference constants and variables from module scope.

4. Functions

  • Only function declarations (no arrow functions, no expressions).
  • Functions cannot be nested inside other functions.
  • Functions have their own scope.
  • No this binding — all functions are standalone.
  • Functions can be passed as values to other functions, e.g. event handlers, but cannot be stored as object properties.

5. System Calls and modules

  • System access is done through intrinsic modules. At compile time the system calls translate to VM opcodes.

  • Intrinsic modules are VM-provided. They:

    • Look like normal modules to the compiler.
    • Use a symbol table in the VM to bind function names to opcodes.
    • Are called with the same mechanism as user-defined functions. In the final binary, syscalls translate to VM opcodes
  • User-declared constants and functions translate to pointers and jump addresses.

    • A function call that does not point to intrinsic functions translate to a VM jump call, with a stack push
  • A module that starts with lib: is considered user-defined, but comes from a library of shared modules, fetched at compile time from a CDN.

import { on, print } from 'sys';
import { gpioRead, gpioWrite } from 'io';
import { displayWrite } from 'lib:ssd1306.es';
import { flower } from './assets/flower.es';

function pinChanged(value) {
  print('Pin value:', value);
  displayWrite(flower);
}

function main() {
  on(0, 'rising', pinChanged);
  gpioWrite(2, 1);
}

6. Control Flow

  • if, else if, else are supported.
  • for, while loops supported.
  • do…while loops not supported.
  • break is supported and compiled into a jumpTo instruction to the loop exit label.
  • continue is supported and compiled into a jumpTo to the loop start label.
  • return is supported to exit from a function.

7. Operators

  • Arithmetic: +, -, *, /, %
  • Comparison: ==, !=, <, <=, >, >=
  • Boolean: &&, || (no short-circuiting; always returns true or false)
  • Assignment: =, +=, -=, *=, /=, %=
  • Increment/Decrement: postfix i++, i-- only (no prefix form).
  • No bitwise operators.
  • No typeof, instanceof, or delete.

Concatenation:

  • number + number → number
  • string + string → string
  • any mix other than the above is a type error
  • buffer + buffer → folded at compile-time
  • any other use of + with a buffer is a compile-time error

8. Types

  • Primitives: number, boolean, string, null

  • Buffer literals: Written as backticks with hex bytes:

    `01ff7a`;
    • Concatenation only allowed between two literal buffers at compile time.
    • No mixing of buffers with variables via +.
  • Arrays:

    • Only literal arrays.
    • Must contain elements of a single type (enforced at compile time).
    • Example: [1, 2, 3] ✅, [1, "a"]
  • Objects:

    • Literal objects allowed.
    • Values must be primitives, arrays, or other objects.
    • Functions as values not allowed.

9. Scope Rules Summary

  • Function scope: Parameters + local variables.
  • Module scope: Top-level variables/constants.
  • No block scope.
  • No closures.
  • No this.

VM Execution Model

1. Bytecode Structure

  • All functions (user-defined and intrinsic) are compiled into a symbol table.
  • Function call instruction:
    • Looks up the symbol name in the table.
    • If found in user space → jump to bytecode address.
    • If found in intrinsic space → execute corresponding VM opcode.

2. Intrinsic Modules

  • Defined in the VM in C.

  • Example symbol table entry:

    { "sys.on", op_iointerrupt },
    { "io.read", op_ioread },
    { "io.write", op_iowrite }
  • The compiler treats these exactly like user functions, but translates them to opcodes.

3. Control Flow

  • Loops and conditionals are compiled into explicit jumpTo instructions.
  • break → jump to the address after the loop block
  • continue → jump to begin adress at the beginning of the loop

4. Program Startup

  • Load all modules into VM memory.
  • Link imports/exports.
  • Find entry module.
  • Call main() — always the program’s first executed function.

Example

// lib/math.es
export function add(a, b) {
  return a + b;
}

// app.es (entry point)
import { add } from './math.es';
import { print } from 'sys';

function main() {
  const sum = add(2, 3);
  print('Sum is:', sum);
}

Testing Checklist

  1. Module rules:

    • Verify library vs entry module restrictions.
  2. Function calls:

    • Call user-defined, imported, and intrinsic functions.
  3. Control flow:

    • Test break and continue in both for and while.
  4. Scope:

    • Confirm no closure behavior.
  5. Types:

    • Reject mixed-type arrays at compile time.
  6. Buffer literals:

    • Concatenate only literal buffers.
  7. Intrinsic handling:

    • Call intrinsic functions like sys.gpio.on as normal functions.

Future Considerations

  • Optional type annotations for better compile-time checks.
  • Static inlining for small user-defined functions.
  • Optimized buffer operations.

About

Espresso Machine -- A tiny virtual machine for esp8266 ☕️

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published