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.
- 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.
Espresso uses familiar JavaScript syntax but with significant restrictions.
- Everything is a module — no global namespace.
- Two kinds of modules:
- Libraries
- Top-level: only
import,export,functionandconstdeclarations. - No
main()requirement.
- Top-level: only
- Entry point modules
- Top-level: only
import,export,functionandconstdeclarations. - Must have a
mainfunction as the program entry point. - No top-level function calls — execution always starts at
main().
- Top-level: only
- Libraries
- 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
- Import and export aliasing (
letandconstare supported.- Variables are function-scoped or module-scoped — no block scope.
varis 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.
- Only function declarations (no arrow functions, no expressions).
- Functions cannot be nested inside other functions.
- Functions have their own scope.
- No
thisbinding — all functions are standalone. - Functions can be passed as values to other functions, e.g. event handlers, but cannot be stored as object properties.
-
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);
}if,else if,elseare supported.for,whileloops supported.do…whileloops not supported.breakis supported and compiled into ajumpToinstruction to the loop exit label.continueis supported and compiled into ajumpToto the loop start label.returnis supported to exit from a function.
- Arithmetic:
+,-,*,/,% - Comparison:
==,!=,<,<=,>,>= - Boolean:
&&,||(no short-circuiting; always returnstrueorfalse) - Assignment:
=,+=,-=,*=,/=,%= - Increment/Decrement: postfix
i++,i--only (no prefix form). - No bitwise operators.
- No
typeof,instanceof, ordelete.
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
-
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.
- Function scope: Parameters + local variables.
- Module scope: Top-level variables/constants.
- No block scope.
- No closures.
- No
this.
- 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.
-
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.
- Loops and conditionals are compiled into explicit
jumpToinstructions. break→ jump to the address after the loop blockcontinue→ jump to begin adress at the beginning of the loop
- Load all modules into VM memory.
- Link imports/exports.
- Find entry module.
- Call
main()— always the program’s first executed function.
// 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);
}-
Module rules:
- Verify library vs entry module restrictions.
-
Function calls:
- Call user-defined, imported, and intrinsic functions.
-
Control flow:
- Test
breakandcontinuein bothforandwhile.
- Test
-
Scope:
- Confirm no closure behavior.
-
Types:
- Reject mixed-type arrays at compile time.
-
Buffer literals:
- Concatenate only literal buffers.
-
Intrinsic handling:
- Call intrinsic functions like
sys.gpio.onas normal functions.
- Call intrinsic functions like
- Optional type annotations for better compile-time checks.
- Static inlining for small user-defined functions.
- Optimized buffer operations.