pkgl
pkgl is an original multi-paradigm programming language with a Python-like
syntax and a WASM-inspired stack-based bytecode VM. The project ships as a
TypeScript-compiled compiler + a VSCode extension that bundles the compiler
for in-editor execution, diagnostics, syntax highlighting, and snippets.
pkgl/
├── package.json # VSCode extension manifest
├── tsconfig.json
├── language-configuration.json # bracket / comment pairing rules
├── syntaxes/
│ └── pkgl.tmLanguage.json # TextMate grammar for highlighting
├── snippets/
│ └── pkgl.json # code templates
├── src/
│ ├── cli.ts # `pkglc` command-line compiler
│ ├── extension.ts # VSCode extension entry
│ └── compiler/
│ ├── index.ts # public API
│ ├── tokens.ts # token types
│ ├── lexer.ts # source -> tokens (handles INDENT/DEDENT)
│ ├── parser.ts # tokens -> AST
│ ├── ast.ts # AST node definitions
│ ├── bytecode.ts # Op enum, Module, disassembler
│ ├── codegen.ts # AST -> bytecode
│ ├── vm.ts # stack-based bytecode VM
│ ├── runtime.ts # Value types (array, map, struct, ...)
│ ├── builtins.ts # print, range, len, read_file, ...
│ └── errors.ts # PkglError + location helpers
├── samples/
│ ├── hello.pkgl
│ ├── fizzbuzz.pkgl
│ ├── functions.pkgl
│ ├── struct.pkgl
│ └── fileio.pkgl
└── build/ # compiled JS output (gitignore in real use)
Quick start
Build
cd pkgl
npm install
npm run build # tsc -> ./build/
Run a program (CLI)
node ./build/cli.js samples/fizzbuzz.pkgl
node ./build/cli.js samples/struct.pkgl --disasm # show bytecode
node ./build/cli.js --help
Install the VSCode extension
This project does not pre-package a .vsix. To use it as an extension
during development:
- Open the
pkgl/ folder in VSCode.
- Press
F5 to launch an Extension Development Host.
- In the host window, open any
.pkgl file (try samples/struct.pkgl).
- Press
F5 again (or run pkgl: Run File from the Command Palette)
to compile and execute the file. Output appears in the pkgl output
channel.
To produce a distributable .vsix, run:
npm install -g @vscode/vsce
vsce package
# -> pkgl-0.1.0.vsix
Then install with code --install-extension pkgl-0.1.0.vsix.
Language tour
Variables
let x = 10 # immutable binding
mut counter = 0 # mutable binding
counter += 1 # compound assignment
let name: string = "pkgl" # with type annotation
Functions
func factorial(n: int) -> int:
if n <= 1:
return 1
return n * factorial(n - 1)
# First-class functions + lambdas
let double = func(x):
return x * 2
print(double(21)) # 42
Control flow
if x > 0:
print("positive")
elif x == 0:
print("zero")
else:
print("negative")
while counter < 10:
counter += 1
for i in range(5):
print(i)
for ch in "hello":
print(ch)
Data structures
let arr = [1, 2, 3]
arr.push(4)
print(arr[0], arr.len())
let m = {"alice": 90, "bob": 75}
for name in m:
print(name, "->", m[name])
Structs and methods
struct Point:
x: int
y: int
func magnitude(self) -> float:
return sqrt(self.x * self.x + self.y * self.y)
func translate(self, dx: int, dy: int) -> Point:
return Point { x: self.x + dx, y: self.y + dy }
let p = Point { x: 3, y: 4 }
print(p.magnitude()) # 5
print(p.translate(10, 0).x) # 13
Error handling
try:
let data = read_file("missing.txt")
catch err:
print("Failed:", err)
# Custom throw
func divide(a, b):
if b == 0:
throw "division by zero"
return a / b
# Line comment
/* Block comment
spanning multiple lines */
Built-in functions
| Category |
Functions |
| I/O |
print, println, input, read_file, write_file, append_file |
| Collections |
len, range, push, pop, keys, values, has, get |
| Math |
sqrt, abs, min, max, sum, floor, ceil |
| Conversion |
int, float, str, bool, type |
| Strings |
split, join, char_at |
Operators
| Category |
Operators |
| Arithmetic |
+ - * / % ** |
| Comparison |
== != < <= > >= |
| Logical |
and or not |
| Bitwise |
& \| ^ ~ << >> |
| Assignment |
= += -= *= /= |
Bytecode VM
The compiler emits a flat bytecode module containing:
- A pool of int / float / string constants.
- A list of function metas (each with its own instruction stream).
- A list of struct type metas.
- A list of globals (function refs and builtins).
The VM is a stack machine with per-frame value stack and local slots.
Exceptions are implemented with a per-frame try-handler stack. There is a
configurable step limit (default 10,000,000) to guard against infinite
loops.
Use --disasm to inspect the bytecode:
$ node ./build/cli.js samples/struct.pkgl --disasm | head -30
=== pkgl bytecode module ===
functions: 7, structs: 2
int pool: [0, 3, 4, 10, 20, 30, 1]
...
---- function 0: main (params=0, locals=10) ----
0 PushInt #1 = 3
1 PushInt #2 = 4
2 MakeStruct Point
3 StoreLocal slot 0
...
VSCode extension features
| Feature |
How |
| Syntax highlighting |
TextMate grammar at syntaxes/pkgl.tmLanguage.json — includes do/new keywords, GLFW builtins, C-style function declarations, property access, named-arg patterns |
| Snippets |
snippets/pkgl.json — try glfwwindow, cfn, do, new, import-file, glfwcreatewindow, dialog, named-id, named-fullsc, frontmost, plus all the originals (fn, if-elif, try, ...) |
| Bracket / comment pairing |
language-configuration.json — also auto-indents after do (x) {, new T(...) {, and int foo(...) { |
| Run file |
Command pkgl: Run File or press F5 in a .pkgl file |
| Run file (GLFW) |
Command pkgl: Run File (with GLFW) or press Shift+F5 — auto-enables the GLFW library and resolves import (file, "x.ph") headers |
| Show bytecode |
Command pkgl: Show Bytecode |
| Live diagnostics |
Real-time parse + compile errors in the Problems pane; respects import (file, "x.ph") header resolution |
| Status bar |
Click pkgl in the status bar to run the current file |
Settings
| Setting |
Default |
Description |
pkgl.diagnostics.enabled |
true |
Enable live syntax diagnostics. |
pkgl.run.showBytecode |
false |
Also print disassembled bytecode when running. |
pkgl.run.maxSteps |
10000000 |
VM step limit (infinite-loop guard). |
pkgl.run.libglfw |
false |
Enable the GLFW builtins when running a file. Equivalent to passing -libglfw to compkg. |
pkgl.run.headerSearchPaths |
[] |
Extra directories to search for .ph header files (e.g. pkglGLFW.ph). Searched in addition to the source file's directory and the workspace root. |
When a source file contains import (file, "pkglGLFW.ph"), the extension automatically:
- Searches the source file's directory, then every workspace folder, then every entry in
pkgl.run.headerSearchPaths.
- Parses the
.ph file for fn <name>( and <type> <name>( declarations.
- Passes the discovered function names to
compile() as glfwBuiltins.
- Auto-enables
libglfw if the file name contains glfw (case-insensitive).
If the header file is missing, a warning is shown in the pkgl output channel (compile-time, not a hard error — so the rest of the file can still be diagnosed).
Architecture notes
- Lexer (
src/compiler/lexer.ts): Produces INDENT/DEDENT/NEWLINE
tokens in addition to the usual lexical tokens, following Python's
off-side rule. Parentheses, brackets, and braces suppress newlines.
- Parser (
src/compiler/parser.ts): Recursive-descent with precedence
climbing for expressions. Block bodies end with a DEDENT token; the
parser tracks a justFinishedBlock flag so a lambda body inside an
outer statement does not require a redundant trailing newline.
- Codegen (
src/compiler/codegen.ts): Single-pass compilation. Each
function gets its own instruction stream. Local slots are allocated
sequentially. Jumps are emitted as placeholders and back-patched.
- VM (
src/compiler/vm.ts): Per-frame instruction pointer and
try-handler stack. Exceptions propagate across frames via a
pendingException field.
Limitations / future work
- No type checker (type annotations are parsed but not enforced).
- No upvalues / captured variables in lambdas (lambdas can only reference
their own params and globals).
- Module imports are limited to
import (file, "x.ph") (header-file form,
used to load builtin function declarations) and import moduleName
(declarative only — does not load external source files).
- No async / concurrency.
- Integer / float distinction is loose (both are JS numbers).
for loop iteration order over maps is insertion order (ES Map).
License
MIT - see headers in source files.