@nicktomlin/tak
v0.1.0
Published
tak — a stack-based language for the browser and command line
Downloads
28
Maintainers
Readme
tak
tak is a small stack-based language that runs in the browser. Inspired by Factor's quotation model, PostScript's dictionary flavor, and a desire for maximum debuggability. The name means "thank you" in Danish — a tribute to the language hackers who came before.
A Taste of tak
In tak, values are pushed onto a stack and words consume and produce them. A program is a sequence of values and words — there are no variables, only the stack.
// Push two numbers and add them
3 4 + // stack: [ 7 ]
dup * // stack: [ 49 ] (square it)
// Functions are defined with a stack effect annotation
fn factorial ( n -- n ) {
dup 1 > [ dup 1 - factorial * ] [ drop 1 ] if
}
5 factorial . // prints 120
// Quotations are first-class — deferred sequences of words
[ 1 2 3 4 5 ] [ dup * ] map . // prints [ 1 4 9 16 25 ]
// Fetch data, parse it, work with the result
"https://api.example.com/data" fetch await response/json
"results" get 0 nth "name" get .Embed it in any page — no install required:
<!-- via CDN -->
<script type="module" src="https://esm.sh/@nicktomlin/tak"></script>
<script type="text/tak">
fn greet ( str -- ) { "Hello, " swap concat . }
"World" greet
</script>Quick Start
Load tak from a CDN — no build step, no install:
<!-- ESM via esm.sh -->
<script type="module" src="https://esm.sh/@nicktomlin/tak"></script>
<!-- or classic script via unpkg -->
<script src="https://unpkg.com/@nicktomlin/tak/dist/tak.js"></script>Then write tak inline:
<script type="text/tak">
fn greet ( str -- ) { "Hello, " swap concat . }
"World" greet
</script>Enable the live debug panel (floating stack overlay):
<script type="module" src="https://esm.sh/@nicktomlin/tak" data-debug="true"></script>Syntax
Literals
42 // integer
3.14 // float
"hello" // string (supports \n \t \r \" \\)
true false // booleans
nullFunction definitions (stack effects required)
fn square ( n -- n ) { dup * }
fn greet ( str -- ) { "Hello, " swap concat . }Quotations / arrays
[ 1 2 3 ] // array of numbers (pushed as a value)
[ 1 + ] // callable quotation
[ "hello" . ] // quotation pushed as a value, called with 'call'Dict literals
{ name: "Alice", age: 30 }Control flow (combinators)
5 0 > [ "positive" . ] [ "non-positive" . ] if
10 [ dup 0 > ] [ 1 - ] while
5 [ "tick" . ] times
true [ "yes" . ] whenJavaScript escape hatch
@js(document.querySelector('#app'))
@js(Math.random())Comments
// line commentStandard Library
Stack
| Word | Effect | Description |
|------|--------|-------------|
| dup | ( a -- a a ) | Duplicate top |
| drop | ( a -- ) | Discard top |
| swap | ( a b -- b a ) | Swap top two |
| over | ( a b -- a b a ) | Copy second |
| rot | ( a b c -- b c a ) | Rotate top three |
| nip | ( a b -- b ) | Drop second |
| tuck | ( a b -- b a b ) | Copy top under second |
Arithmetic
+ - * / % ** neg
Comparison → boolean
= != < > <= >=
Logic
and or not
String
| Word | Effect |
|------|--------|
| concat | ( str str -- str ) |
| length | ( str\|arr -- n ) |
| substr | ( str start end -- str ) |
| split | ( str sep -- arr ) |
Array / Quotation
| Word | Effect |
|------|--------|
| push | ( arr val -- arr ) |
| pop | ( arr -- arr val ) |
| nth | ( arr n -- val ) |
| map | ( arr quot -- arr ) |
| filter | ( arr quot -- arr ) |
| reduce | ( arr init quot -- val ) |
| each | ( arr quot -- ) |
| call | ( quot -- ) |
Dict
| Word | Effect |
|------|--------|
| get | ( dict key -- val ) |
| set | ( dict key val -- dict ) |
| has | ( dict key -- bool ) |
| keys | ( dict -- arr ) |
Control
| Word | Effect |
|------|--------|
| if | ( bool true-q false-q -- ) |
| when | ( bool quot -- ) |
| unless | ( bool quot -- ) |
| while | ( cond-q body-q -- ) |
| times | ( n quot -- ) |
I/O
./log— print top of stack to consolestr—( val -- str )— convert any value to its string representation
Type
type— push type name as string:"number","string","bool","array","dict","quot","null","element"number? string? bool? array? dict? null? quot?
Async / Fetch
| Word | Effect | Description |
|------|--------|-------------|
| fetch | ( url -- Promise ) | HTTP GET |
| fetch-post | ( url body -- Promise ) | HTTP POST; body can be string or dict |
| await | ( Promise -- val ) | Resolve a promise |
| response/json | ( Response -- val ) | Read response body as JSON → tak value |
| response/text | ( Response -- str ) | Read response body as plain text |
| json/parse | ( str -- val ) | Parse a JSON string |
| json/str | ( val -- str ) | Serialize a tak value to JSON |
"https://example.com/api" fetch await response/json // → tak dict/array
"https://example.com/api" fetch await response/text // → string
"https://example.com/api" { x: 1 } fetch-post await response/json
'{"x":1}' json/parse
{ x: 1 } json/strJS Interop
| Word | Effect | Description |
|------|--------|-------------|
| import | ( url -- Promise ) | Dynamic JS module import (async; use await) |
| js/get | ( js key -- val ) | Read a property from a JS object |
| js/call | ( js arg1..argN n -- result ) | Call a JS function with N args |
| js/unwrap | ( js -- val ) | Convert a JS value to a tak value |
"https://cdn.example.com/lib.js" import await // ( -- js-module )
"Chart" js/get // ( js-module -- Chart )
0 js/call // call with 0 argsStatic Imports (use)
use is a hoisted import — all URLs are fetched concurrently before the program runs, with results available synchronously at the call site.
"./lib.js" use ( -- module )
"./lib.js" [ Chart ] use // defines word `Chart`
"./lib.js" [ renderFn as render ] use // defines word `render`
"./lib.js" [ Chart renderFn as render ] useThe URL must be a string literal (known at parse time). Without a binding list, the whole module is pushed onto the stack. With bindings, each named export becomes a word that pushes its value.
Debug
trace— log top value without consuming itdebug— fire debug event + log full stack
DOM
| Word | Effect |
|------|--------|
| dom/query | ( selector -- el\|null ) |
| dom/query-all | ( selector -- arr ) |
| dom/create | ( tag -- el ) |
| dom/append | ( parent child -- parent ) |
| dom/text | ( el str -- el ) |
| dom/html | ( el str -- el ) |
| dom/on | ( el event quot -- el ) |
| dom/attr-get | ( el attr -- val ) |
| dom/attr-set | ( el attr val -- el ) |
| dom/class-add | ( el class -- el ) |
| dom/class-rm | ( el class -- el ) |
| dom/remove | ( el -- ) |
dom/on pushes the element onto the stack before calling the quotation.
Debug Panel API
const interp = new Tak.TakInterpreter({ debug: true });
interp.on('beforeWord', ({ word, stack }) => { });
interp.on('afterWord', ({ word, stack }) => { });
interp.on('push', ({ value, stack }) => { });
interp.on('pop', ({ value, stack }) => { });
interp.on('error', ({ error, word, stack }) => { });
interp.on('scriptEnd', ({ stack }) => { });
interp.stack // read-only snapshot
interp.dict // Map of all defined wordsCtrl+D toggles the debug overlay when data-debug="true" is set.
Programmatic Use
import { TakInterpreter, registerStdlib, registerDomStdlib } from './dist/tak.js';
import { tokenize } from './dist/tak.js';
import { parse } from './dist/tak.js';
const interp = new TakInterpreter({ debug: true });
registerStdlib(interp);
registerDomStdlib(interp);
const tokens = tokenize('5 3 + .');
const program = parse(tokens);
await interp.run(program);Project Structure
src/
lexer.ts — tokenizer
parser.ts — recursive-descent parser
ast.ts — AST node types
interpreter.ts — tree-walking interpreter + event system
stdlib.ts — built-in words
dom-stdlib.ts — DOM words
debug-panel.ts — floating stack overlay
browser-runtime.ts — script tag scanner + bootstrap
examples/
hello.html — stack ops, fn defs, combinators
counter.html — DOM event handling
fetch.html — async fetch with debug panelBuild
npm install
npm run build # production bundle → dist/tak.js
npm run build:dev # with source maps
npm run watch # watch mode
npm run typecheck # tsc type check onlyLicense
MIT
