Language Tooling / Systems / Research Notes
JaggerScript: A Browser-Runnable Typed Scripting Language
A compact language implementation that owns the grammar, typed intermediate representation, heap-backed interpreter, examples, diagnostics, and browser playground.
Abstract
JaggerScript is a small typed scripting language implemented in TypeScript. The system includes a PEG grammar, a compiler pass that converts parser output into a typed intermediate program representation, an interpreter with stack frames and heap-backed object instances, and a browser playground powered by Monaco. The language supports classes, constructors, functions, primitive values, arithmetic and boolean expressions, conditional blocks, loops, break, return, instantiation, scoped member access, and console output examples. This paper explains the implementation as a language-tooling system: a deliberate separation between parsing, normalization, runtime evaluation, and the interface used to explore the language.
I
Motivation
JaggerScript is less about inventing a production language and more about owning the whole language pipeline. It starts with source text, passes through a PEG grammar, normalizes into explicit program structures, and then executes through an interpreter whose state can be inspected and explained.
The separation pays for itself at failure boundaries. Grammar bugs produce malformed parse trees, compiler bugs produce incorrect normalized tokens, runtime bugs corrupt state, and interface bugs make the language hard to explore. The browser playground exposes those boundaries directly.
The right mental model is a small object-oriented language laboratory. The project is not competing with JavaScript, TypeScript, or Java. It is a controlled environment for showing how source text becomes executable behavior: how a grammar recognizes a program, how a compiler gives the parse tree a cleaner runtime shape, and how an interpreter manages variables, references, calls, and errors.
- Primary goal: implement the full parser-to-runtime loop in TypeScript.
- User goal: make language behavior explorable without local setup.
- Design constraint: keep the runtime small enough that examples are easy to reason about.
II
Language Model
The language is class-oriented and deliberately compact. A program is a list of classes. Classes own global definitions, functions, and a constructor. Function bodies then contain definitions, reassignment, control flow, return, break, instantiation, function calls, variables, and expressions.
The type vocabulary is intentionally small: numbers, strings, booleans, and object instances. The interpreter stays focused on scope, references, object allocation, and expression evaluation rather than a large standard library.
The language therefore sits between a calculator language and a production scripting language. It has enough structure to demonstrate object identity, method calls, mutation, loops, and nested references, but it avoids modules, inheritance, generics, arrays, closures, and asynchronous behavior. Those omissions keep the runtime state small enough to inspect directly.
- Classes and constructors provide object allocation and initialization.
- Functions, while loops, if/elif/else blocks, break, and return provide imperative control flow.
- Scoped member access supports object graphs such as linked lists and nested instances.
- Example programs cover FizzBuzz, cube-root iteration, linked lists, nested classes, references, and randomized experiments.
III
Parser and Compiler Pass
The parser grammar is written in PEG form and emits plain parser nodes with token-type labels. A separate compiler pass converts those raw nodes into a typed program representation, turning a syntax-shaped tree into runtime-shaped data.
That division keeps the grammar from becoming the whole language implementation. Parsing answers whether the input has the right shape; compilation decides what each shape means to the interpreter.
The first major tradeoff is a normalization pass. A one-pass interpreter could walk the parser output directly, but parser helper nodes and incidental grammar structure would leak into runtime code. JaggerScript pays that extra pass so the interpreter can consume a smaller vocabulary whose nodes are named for behavior rather than syntax.
- Raw nodes use names such as Class, Definition, Function, IfBlock, WhileLoop, FuncCall, and Primitive.
- Compiled tokens use a smaller enum-like runtime vocabulary.
- Expression conversion handles primitives, arithmetic expressions, boolean expressions, function calls, variables, and instantiation.
- Comments are filtered from the executable program representation.
IV
Grammar and Surface Semantics
The grammar is PEG-based and statement-oriented. A source file is a sequence of class declarations and comments. Class bodies contain field definitions, functions, and a constructor. Function bodies contain definitions, reassignment, function calls, object construction, conditionals, loops, break, and return.
class Counter {
number value = 0;
constructor(number start) {
value = start;
}
func number next() {
value = value + 1;
return value;
}
}Arithmetic expressions use conventional precedence by parsing additive tails over multiplicative terms. Boolean expressions compare two expressions with relational or equality operators. Function calls and scoped variables share the same scope prefix representation, which lets method calls and member access reuse the same scope-walk machinery.
V
Typed Program Representation
The compiler output is a discriminated token graph. It is not a direct parser tree: every node receives a runtime token kind and only the fields needed by evaluation. Parser details such as whitespace and grammar helper productions are removed before interpretation, so reconstruction follows the runtime model instead of the grammar.
Program = { token: Program, classes: Class[] }
Class = {
token: Class;
name: string;
globalVars: Definition[];
functions: Func[];
construct?: Constructor;
}
Func = {
token: Func;
parent: string;
name: string;
args?: ArgsDefine;
within: Token[];
}
Variable = {
token: Variable;
scope?: ScopeSpec;
variableName: string;
type: ValueType;
typeStr: string;
}Definitions carry declared type strings; primitives are converted into typed Value nodes; arithmetic operators are represented as left expression plus ordered operation tail; function-call arguments are normalized into arrays even when the source contains one argument.
VI
Interpreter Runtime
The interpreter state is built around a heap, a stack of function frames, a stack pointer, the currently running instance, class definitions, and function maps. Object instances hold a pointer and a global scope that maps field names to heap pointers.
That model gives the language reference behavior without pretending JavaScript variables are the runtime. Local variables live in stack frames; object fields live through heap-backed instance scopes; scoped access walks object references until it reaches the requested field.
Program | v class table C -------- function table F | | v v running instance i ---- stack frame K[sp] | | v v globalScope: name -> ptr locals: name -> Value | v heap H: ptr -> Value
Assignments preserve the existing storage discipline. Local writes update the active frame. Field writes walk the scope path to the target instance and then update or replace the heap value behind the field pointer. If the existing field type and assigned value type disagree, the runtime rejects the assignment.
VII
Allocation and Object Identity
Object construction allocates an Instance value on the heap, temporarily sets it as the running instance, evaluates global field definitions, restores the prior running instance, and finally evaluates the constructor body against the new instance.
new Class(args) | v allocate pointer p | v heap[p] = Value(Instance) | v runningInstance = heap[p] | v evaluate field definitions into globalScope | v evaluate constructor(args) | v return Instance pointer
Primitive values can be stored directly in stack frames, but object fields point into the heap. Reassignment to a field either reuses an existing heap-backed value or allocates a new heap slot and updates the owning instance's globalScope mapping.
VIII
Evaluation Semantics
Evaluation is state-transforming. Most executable tokens consume an interpreter state and return an updated state plus, when applicable, a value. Function invocation creates a new frame, binds arguments, evaluates the function body in order, and unwinds on return. Constructors reuse the same mechanism while temporarily switching the running instance.
FuncCall | v resolve receiver scope | v lookup function in class table | v push stack frame and bind args | v evaluate body until return/break/end | v pop frame and restore running instance
Break is represented as a control signal rather than as an ordinary value. Loop evaluation catches the break signal and exits the nearest loop; return exits the current function body with the produced value.
IX
Expression Semantics
Expression evaluation is strict and eager. Arithmetic expressions evaluate the left operand, then each operation tail in order. Boolean expressions evaluate both sides and apply the relational or equality operator. Function calls evaluate argument expressions before binding them to the callee's parameter names.
doll.inner.value
1. resolve doll in local frame or running instance scope
2. assert doll is an Instance
3. follow inner through doll.globalScope
4. assert inner is an Instance
5. read value from inner.globalScopeX
Browser Playground
The browser integration turns the interpreter into a browser-native playground. Monaco provides the editable source surface, a Monarch tokenizer gives JaggerScript-specific highlighting, and a diagnostics pass marks syntax problems as the user edits.
The examples rail is part of the technical interface. Small runnable programs exercise specific runtime behaviors and provide stable fixtures for understanding parser, compiler, and interpreter output.
- Built-in examples include FizzBuzz, CubeRoot, DoublyLinkedList, DoubleBreak, OnlyTheEvens, NestingDollClasses, AllocateAnInstance, GetTheReference, and Random.
- The Run action parses, compiles, evaluates, and prints interpreter output.
- The Reset action restores the selected example source.
- The browser route makes the language testable without local installation.
XI
Diagnostics and Editor Tooling
The browser integration wraps the parser with Monaco diagnostics. Parse failures are converted into editor markers with line and column ranges. A lightweight semantic pass also identifies unknown declared types by comparing declarations against primitive type names and class names found in the current source.
Syntax highlighting is supplied through a Monarch tokenizer. Keywords, type keywords, function names, console calls, identifiers, class names, strings, numbers, comments, delimiters, and operators receive separate token classes. The editor route therefore presents the language as an owned tool rather than plain text in a generic textarea.
- The Check action parses and reports diagnostics without running the program.
- The Run action captures console output and restores the original console hooks after execution.
- Examples are imported as raw source files so the playground and repository tests share the same corpus.
- Editor theme state is persisted locally and does not affect runtime behavior.
XII
Runtime Errors and Safety Checks
The interpreter rejects invalid runtime states immediately. Missing variables, non-instance scoped access, unknown fields, and type-incompatible field assignments raise errors instead of returning undefined values. The language has explicit type annotations but no full static checker, so invalid states must fail at the point of use.
- Unbound local or field names produce a missing-variable error.
- Attempting to traverse a primitive as an instance produces an instance assertion error.
- Assigning a value whose typeStr differs from an existing field type is rejected.
- Break is only meaningful inside loop evaluation and is represented as a control exception.
- Console output is captured and formatted by the browser runtime wrapper.
XIII
Tradeoffs and Missing Pieces
JaggerScript performs only lightweight static checking in the browser. It can catch parse errors and unknown declared type names before execution, but it does not prove every expression type statically. Instead, many safety checks occur at runtime: scoped member access must traverse instances, field assignment must preserve the existing type string, and missing variables throw immediately.
That choice keeps the project centered on interpreter mechanics. A full static type checker would be a valuable next layer, but it would also change the paper's main teaching object from parser/runtime design to type-system design. The current version makes the runtime consequences of each statement visible and keeps examples small enough to follow by hand.
- The compiler normalizes parser output, but it is not a full optimizer.
- Heap-backed instances model reference identity, but memory management is intentionally simple.
- Runtime errors are explicit, but many could become static errors in a richer checker.
- The Monaco playground is a learning and debugging surface, not a production IDE.
XIV
Results and Design Properties
JaggerScript demonstrates a complete small-language toolchain in TypeScript: a formal grammar, compiler normalization pass, explicit runtime state, class and instance semantics, scope traversal, function frames, source diagnostics, syntax highlighting, and a browser execution surface.
The technical value is the separation of responsibilities. The parser recognizes syntax, the compiler gives syntax a runtime shape, the interpreter owns state transitions, and the editor projects parser/runtime results back to the user. That structure is sufficient to reconstruct the current language without treating the playground as a special case.
- The language supports class-based object programs with constructors, fields, functions, loops, branches, return, break, allocation, reassignment, and scoped member access.
- Heap-backed instances preserve object identity across local frames and nested references.
- The compiler pass prevents parser-node shape from leaking directly into the interpreter.
- The Monaco integration makes parse and type-name feedback available at edit time.