Write quickly with dynamic values, then add hints and interfaces where contracts matter.
Tiny Programming Language
Readable scripts, compact bytecode, Go-powered runtime.
Tiny is a dynamic language with optional static hints, classes, modules, structural interfaces, native concurrency, and a production-minded standard library. Source files compile into `.tbc` bytecode and run on a custom stack-based VM written in Go.
import std "io"
import std "json"
interface Task {
title: string
done: bool
}
fn complete(task: Task) {
return {
message: `Completing ${task.title}`,
ok: task.done == false
}
}
io.println(json.pretty(complete({
title: "Write docs",
done: false
})))
Tiny compiles source into binary bytecode with slot-based locals and optimization passes.
`spawn`, `await`, `lock`, and mutexes make concurrent scripts feel direct.
Package scripts, bytecode, embedded assets, and native plugins into distributable builds.
01
Install
Tiny is distributed as a standalone executable. Put the binary somewhere on your `PATH`, then verify it with `tiny version`.
Use a release binary
Download the Tiny executable for your platform, place it in a tools folder, and make sure that folder is available from your terminal.
# Windows
tiny.exe version
# Linux
./tiny_linux version
# Mac
./tiny_darwin version
Build on Windows
The repository includes a Windows build script that builds the runtime and compiler executable.
build.bat
tiny version
Build on Linux
The Linux build script produces `tiny_linux` and the embedded Linux runtime.
chmod +x ./build_linux.sh
./build_linux.sh
./tiny_linux version
Build on Mac
The Mac build script produces `tiny_darwin` and the embedded Darwin runtime.
chmod +x ./build_mac.sh
./build_darwin.sh
./tiny_darwin version
Command check
`tiny version` prints the language version and bytecode cache version. If this command works, the CLI is installed correctly.
tiny version
Normal Tiny programs do not need Go or TinyGo at runtime. `native fn` blocks require Go and TinyGo on the machine compiling those native functions.
02
Quick Start
Run Tiny from a project folder with a `tiny.json` file, or pass a source file directly. The examples in this repository are ready to run.
Create a source file
import std "io"
const name = "Tiny"
io.println(`Hello from ${name}`)
Run the program
# From a project with tiny.json
tiny
# Or run a file directly
tiny src/main.tiny
Explore examples
cd examples/01-basics
../../tiny
Example projects include `tiny.json`; when you run `tiny` inside one of those folders, the tool resolves the configured entrypoint and executes it.
03
Project Layout
A Tiny project is usually small: one config file, source files, optional assets, and generated bytecode cache files.
Typical structure
my-tool/
tiny.json
src/
main.tiny
helpers.tiny
ui/
index.html
data/
input.json
Generated files
Tiny may create `.tinycache` folders containing compiled `.tbc` bytecode. These files speed repeated runs and are also the format used by the runtime.
For distribution, use `tiny pack` or `tiny dist` instead of manually copying cache files.
04
Language Basics
Tiny syntax is familiar if you know JavaScript, Lua, or Python, but it has its own compact style: braces for blocks, optional semicolons, and direct standard-library imports.
Variables
Use `let` for mutable bindings and `const` for values that should not be reassigned.
const language: string = "Tiny"
let count: number = 3
count = count + 1
count += 2
count++
Values
Core values include numbers, strings, booleans, arrays, objects, functions, classes, buffers, errors, and `null`.
let enabled = true
let empty = null
let words = ["tiny", "docs"]
let user = {
name: "Ada",
active: enabled
}
Strings
Backtick strings support interpolation with `${...}` expressions.
const name = "Tiny"
const version = 1
let label = `${name} docs v${version}`
Objects and arrays
Use dot access for named fields and bracket access for dynamic keys.
let numbers = [1, 2, 3]
numbers.push(4)
let user = { name: "Sam" }
user.role = "reader"
user["level"] = "beginner"
05
Types & Interfaces
Tiny is dynamic by default, with runtime-checked type hints for variables, parameters, returns, and structural interfaces.
Type hints
fn add(a: number, b: number): number {
return a + b
}
let name: string = "Tiny"
let handler: function = fn(value) {
return value
}
Common hint names include `string`, `number`, `bool`, `array`, `object`, `function`, `buffer`, `error`, `null`, and `any`.
Interfaces
interface Request {
path: string
method: string
params: object
}
fn logRequest(req: Request) {
return `${req.method} ${req.path}`
}
Interfaces are structural: an object is valid when it has the required shape, regardless of where it was created.
Standard library stubs use unions such as `string | function` and optional parameters like `debug?: bool`, so public APIs can express flexible call patterns.
06
Control Flow
Tiny supports familiar imperative control flow, plus `match` and enums for cleaner branching.
If and loops
while index < values.length() {
const value = values.get(index)
index = index + 1
if value == 5 {
continue
}
if value > 6 {
break
}
}
Classic `for`
for let i = 0; i < values.length(); i++ {
io.println(values.get(i))
}
`for in`
for value in values {
io.println(value)
}
Match
match state {
"Small" {
io.println("small")
}
"Medium" {
io.println("medium")
}
_ { // default
io.println("large")
}
}
Enums
enum State {
Small = "small",
Medium = "medium",
Large = "large"
}
enum State {
Small = 1,
Medium = 2,
Large = 3
}
enum State {
Small = iota, // starts from 0
Medium,
Large
}
enum State {
Small, // becomes "Small"
Medium,
Large
}
let current = State.Small
07
Functions
Functions are first-class values. You can pass them as callbacks, return them from other functions, and close over local state.
Named functions
fn greet(name, prefix = "Hello") {
return `${prefix}, ${name}`
}
io.println(greet("Tiny"))
io.println(greet("Tiny", "Welcome"))
Closures
fn makeCounter(start) {
let current = start
return fn() {
current = current + 1
return current
}
}
let next = makeCounter(10)
08
Modules
Tiny has standard-library imports and file imports. File modules expose only values marked with `export`.
Standard modules
import std "io"
import std "json"
io.println(json.pretty({ ok: true }))
File modules
import "numbers.tiny" as Numbers
io.println(Numbers.sum([2, 4, 6]))
Exports
export const defaultStep = 1
export fn sum(values): number {
let total = 0
for value in values {
total += value
}
return total
}
Plugin imports
The compiler includes native plugin support for external DLL/SO integrations. Plugins are imported separately from standard and file modules.
09
Classes
Classes provide constructors, fields, methods, `this`, method chaining, embedded objects, and `instanceof` checks.
class Log {
field messages = []
fn init() {
this.messages = []
}
fn add(message) {
this.messages.push(message)
return this
}
}
class Box {
field value = null
embed log
fn init(value) {
this.value = value
this.log = Log()
this.add("created")
}
}
let box = Box("start")
io.println(box instanceof Box)
10
Libraries
Tiny has built-in package management to add, install, list, and remove external libraries from GitHub, including support for native plugins.
Installing libraries
Use tiny add to add a dependency to your tiny.json and download it. You can specify a custom alias name if desired.
# Install library using default repo name
tiny add github:owner/repo
# Install library using a specific alias name
tiny add aliasName github:owner/repo
# Install using a specific ref (branch, tag, or commit)
tiny add github:owner/repo@v1.0.0
Run tiny install to download all dependencies declared in your project's tiny.json file.
tiny install
Removing & Listing
Use tiny remove (or tiny rm) to delete a dependency from your project and its downloaded files.
# Remove project dependency and its global cache
tiny remove aliasName
# Remove dependency from project only
tiny remove aliasName --project-only
# Remove globally downloaded library cache
tiny remove owner/repo --global
Use tiny deps (or tiny list) to list all globally downloaded libraries and their versions.
tiny deps
Using libraries
Import library modules using the import lib syntax. Use "owner/repo" as the import path and provide an alias name.
import lib "owner/repo" as MyLib
Example: TinyJWT
This example demonstrates how to install and use TinyJWT for signing and verifying tokens.
tiny add github:confh/TinyJWT
import lib "confh/TinyJWT" as Jwt
import std "io";
import std "time";
const secret = "7T]kC&'.d&1|PC0M$]>vH~:"
const jwt = Jwt.JWT(secret)
const token = jwt.sign({
message: "hello"
}, 3)
io.println(`Token: ${token}`)
io.println(`Verification: ${jwt.verify(token)}`)
// wait 4 seconds for the token to expire
time.sleep(4000)
io.println(`Verification: ${jwt.verify(token)}`)
Creating libraries
A library is a standard Tiny project that contains a tiny.json file at its root. It is critical that the "entry" field in tiny.json is set to the default .tiny file path that consumer imports will point to.
{
"entry": "src/jwt.tiny",
"dependencies": {},
"plugins": []
}
Packaging plugins
If your library requires native compiled plugins (e.g. .dll, .so, or .dylib files), define them under the "plugins" list in your library's tiny.json.
When compiling your native plugins for distribution, upload the compiled binaries for all target platforms as assets on your GitHub releases. When consumers run tiny install, the package manager will automatically download the correct native plugin binary matching their target platform from the latest GitHub Release.
11
Errors & Defer
Tiny includes exceptions for recoverable runtime failures and `defer` for cleanup just before a function returns.
Try, catch, finally
import std "error"
try {
throw error.new("ValidationError", "path is required")
} catch err {
io.println(err.kind)
io.println(err.message)
} finally {
io.println("done")
}
Defer cleanup
fn runSimulation() {
io.println("Opening resource")
defer fn() {
io.println("Closing resource")
}
io.println("Working")
}
Current parser behavior permits one `defer` statement per function scope.
12
Concurrency
Tiny exposes Go-backed concurrency through language syntax and standard library primitives. Spawned tasks run with isolated VM state, and shared mutations can be protected with locks.
import std "time"
import std "sync"
let mutex = sync.mutex()
let task = spawn () fn() {
time.sleep(1000)
return "parallel result"
}
lock mutex {
io.println("safe section")
}
io.println(await task)
Concurrency tools
- `spawn` starts work in another VM execution space.
- `await` waits for a spawned task and returns its result.
- `sync.mutex()` creates a mutex object.
- `lock mutex { ... }` acquires and releases around a block.
- `http.server(...).start(true)` can run server work asynchronously.
13
Embedding Assets
Embed data at compile time so scripts and packaged executables can carry configuration, binary files, or whole UI folders.
Text
embedstr "./data.json" const data
io.println(data)
Binary
embedbin "./icon.png" const iconBytes
fs.writeBytes("icon-copy.png", iconBytes)
Directory
embeddir "./ui" const assets
const html = assets["index.html"]
Use cases
Bundled webview HTML, config files, templates, images, and other static resources that should travel with a distributed tool.
14
Native Go Functions
`native fn` lets you write performance-sensitive logic in Go inside Tiny source. Native blocks are compiled through TinyGo to WebAssembly and cached.
native fn gosha256(input: string): string {
go {
import "crypto/sha256"
import "encoding/hex"
h := sha256.Sum256([]byte(input))
return hex.EncodeToString(h[:])
}
}
Requirements
Native functions require Go and TinyGo installed on the host machine. They are best for tight loops, hashing, parsing, math-heavy work, or targeted use of Go packages.
Because native functions cross a runtime boundary, keep their API narrow and pass simple data where possible.
15
Standard Library
The standard library is broad enough for scripts, servers, automation, tests, desktop tools, and GUI shells.
16
Examples
The repository includes focused examples that double as a learning path.
17
Plugin Development
Native plugins let Tiny call external shared libraries while keeping Tiny programs pleasant to use. A good plugin has two layers: a native `.dll` or `.so` that speaks Tiny's JSON call protocol, and a `.tiny` wrapper module that exposes a typed, documented Tiny interface.
Raw plugin calls are intentionally low-level. Put `import plugin ...` inside a `.tiny` wrapper, then export functions, enums, interfaces, and classes that Tiny users can call normally. That wrapper is where you define Tiny-facing type hints, object shapes, defaults, validation, and friendly names.
What Tiny loads
Tiny loads platform shared libraries. On Windows, paths without an extension resolve as `.dll`; on Linux, they resolve as `.so` and Darwin as `.dylib`.
import plugin "./hash" as hashPlugin
The runtime searches the current working directory, the executable directory, and registered import/source folders.
Required exports
The native library must export exactly the functions Tiny looks for: `TinyPluginCall` and `TinyPluginFree`.
// Tiny calls this with a method name and JSON payload.
TinyPluginCall(method, argsJSON) -> char*
// Tiny calls this after reading the returned string.
TinyPluginFree(ptr)
If either symbol is missing, plugin loading fails before any method is called.
Call payload
Each call is serialized as JSON. Tiny sends the method name separately and also includes it in the payload.
{
"method": "hash",
"args": [
{
"text": "Tiny",
"method": "sha256"
}
]
}
Return payload
Return normal JSON-compatible values: strings, numbers, booleans, arrays, objects, or `null`. Tiny maps the JSON result back into Tiny values.
{
"success": true,
"method": "sha256",
"result": "..."
}
Native Go plugin shape
A Go plugin can use the repository's `tinyplugin` helper package to register named handlers and handle the JSON request/response protocol.
package main
/*
#include
*/
import "C"
import (
"unsafe"
"language.com/src/tinyplugin"
)
func init() {
tinyplugin.Register("hash", func(args tinyplugin.Args) (any, error) {
config := args.Object(0)
text := config["text"].(string)
return map[string]any{
"success": true,
"input": text,
}, nil
})
}
//export TinyPluginCall
func TinyPluginCall(methodC *C.char, argsC *C.char) *C.char {
method := C.GoString(methodC)
argsJSON := C.GoString(argsC)
return C.CString(tinyplugin.HandleCall(method, argsJSON))
}
//export TinyPluginFree
func TinyPluginFree(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}
func main() {}
Error contract
Plugins report failures by returning an object with an `error` field. Tiny turns that into a Tiny runtime error with the given kind and message.
{
"error": {
"kind": "ValidationError",
"message": "Missing text field"
}
}
Handle state safely
If your plugin owns long-lived native resources, return opaque handles to Tiny instead of raw pointers. The helper package includes a handle store pattern for mapping IDs like `file_1` or `socket_2` to native values.
let handle = filePlugin.open("data.txt")
filePlugin.close(handle)
Tiny wrapper module
The wrapper is the public API. It hides the raw native namespace, validates Tiny inputs, and gives the LSP type hints and structural interfaces to work with.
import plugin "./hash" as hashPlugin
export enum HashMethod {
md5,
sha256
}
export interface HashResult {
success: bool
method: string
input: string
result: string
}
export fn hash(text: string, method: string): HashResult {
return hashPlugin.hash({
text: text,
method: method
})
}
Wrapper responsibilities
Expose a Tiny-shaped API, not a native-shaped API. Prefer objects with named fields over positional argument lists when a call has several options.
Type the boundary
Use `interface`, `enum`, and return type hints in the `.tiny` wrapper so callers get validation and editor help before the native library is involved.
Keep JSON-compatible data
The native boundary is JSON-based. Pass simple Tiny values, arrays, and objects. Represent native resources as string handles.
Distribution
`tiny dist` scans plugin imports and literal `Plugin.load("...")` calls, copies plugin files into the release, and rewrites paths for the target package.
18
Runtime Model
Tiny separates the developer-facing language from the runtime that executes it. Source files compile into a compact bytecode format, then the VM runs that bytecode with access to standard modules, plugins, and native functions.
Cache path
Normal runs may create `.tinycache` bytecode beside the entry file. The cache is an implementation detail; source files remain the normal authoring format.
Distribution path
`tiny pack` and `tiny dist` turn bytecode, runtime assets, embeds, and plugin dependencies into deployable artifacts.
Concurrency path
`spawn` runs work in isolated VM state, while shared resources should be protected by `sync.mutex()` and `lock` blocks.
Native boundary
`native fn` compiles Go snippets through TinyGo/WASM, while plugins communicate with the VM through a JSON-compatible ABI.
19
Gotchas
These are the small details worth knowing before building larger Tiny projects.
Use `tiny version`
The version command is `tiny version`, with aliases `tiny ver` and `tiny v`. It is not `tiny --version`.
One defer per scope
The current parser permits one `defer` statement per function scope. Use one cleanup function that performs all cleanup work.
Plugins are JSON-boundary APIs
Keep plugin arguments and results JSON-compatible. Use string handles for native resources rather than exposing raw pointers to Tiny.
Ship plugin wrappers
Plugin users should import `.tiny` wrapper modules, not call raw native plugin methods directly. Wrappers provide types, defaults, validation, and editor help.
Cache is not your API
`.tinycache` files are generated artifacts. Do not edit them by hand; rebuild from source or use `--disable-cache` while debugging cache behavior.
Native functions need tooling
`native fn` requires Go and TinyGo where the native code is compiled. Plain Tiny scripts and packaged runtime execution do not require those tools.
20
Packaging & Deploy
Tiny can ship scripts as bytecode or bundle them into standalone native executables with the runtime.
Compile and run bytecode
Source is compiled into `.tbc` bytecode and executed by the VM. Cache files live under `.tinycache`.
Pack a standalone tool
tiny pack src/main.tiny -o mytool
`pack` bundles bytecode and the Tiny runtime into one native executable.
Distribute with native dependencies
tiny dist src/main.tiny -o release/app
`dist` resolves plugin/runtime assets and produces a clean release folder.
Windowed apps
tiny dist --windowed
Use this for GUI apps on Windows when you do not want a console window.
21
Editor Support
The project includes a Tiny language server with diagnostics, formatting, autocomplete, type analysis, and jump-to-definition support. The LSP also ships stubs for the standard library so editors can understand `std` imports.