Skip to content

Instantly share code, notes, and snippets.

@timm
Last active June 27, 2026 01:38
Show Gist options
  • Select an option

  • Save timm/47dfa0f6ab796999410afa3c0dcbdae2 to your computer and use it in GitHub Desktop.

Select an option

Save timm/47dfa0f6ab796999410afa3c0dcbdae2 to your computer and use it in GitHub Desktop.
,fun.md

Purpose Author Language License

QR

luk is the .luk language: a tiny indentation-based dialect that transpiles to Lua via luk.lua (~100-line module). luk.lua returns a single function: local lua_src = require("luk")(fun_src). Same Lua semantics, fewer ends, Python-style list comprehensions.

git clone http://tiny.cc/luk && cd luk
# transpile (luk.lua is a module; one-liner driver):
lua -e 'io.write(require("luk")(io.read("*a")))' < my.luk > my.lua
lua my.lua                            # run
make my.lua                           # via Makefile

For the optimizer shipped with luk (fft.luk) see fft.md.

Sections: NAME | SYNOPSIS | LANGUAGE REFERENCE | PERFORMANCE | FILES | VIM SUPPORT | SEE ALSO | LICENSE | AUTHOR

Files: luk.lua | fft.luk | lib.luk | fft.lua | lib.lua | fft.md | luk.rc | luk.vim

NAME

luk - .luk-to-Lua transpiler (single-file, no deps)

SYNOPSIS

lua -e 'io.write(require"luk"(io.read"*a"))' <IN.luk >OUT.lua
-- or programmatically:
--   local lua_src = require("luk")(fun_src)

LANGUAGE REFERENCE

Keywords (whole-word substitution)

fun                -> function
!                  -> return
!=                 -> ~=     (Lua's not-equal)

Local declarations

NAME := EXPR       -> local NAME = EXPR
A, B := X, Y       -> local A, B = X, Y

Compound assignment (start of line)

X += V             -> X = X + V
X -= V             -> X = X - V
X *= V             -> X = X * V
X /= V             -> X = X / V

Block openers (use ":" at EOL or before body)

if (cond):         -> if cond then
elseif (cond):     -> elseif cond then
else:              -> else
for X in Y:        -> for X in Y do
for i = a, b:      -> for i = a, b do
while cond:        -> while cond do
fun (args):        -> function(args)
NAME := fun (a):   -> local NAME = function(a)

Block bodies

  • Same line after : = one-liner, auto-appends end.

  • Indented next lines = multi-line; outdent emits end.

  • Continuation lines starting with else/elseif do NOT trigger the outdent close.

  • Lone end lines are folded onto the previous code line (skipping blank lines and comments).

  • Inline anonymous fun in expressions needs explicit end:

    cb := fun (x): ! x*2 end
    

List comprehensions (Python-style, inside [ ])

[EXPR for V in ITER]
[EXPR for V in ITER if COND]
[EXPR for K,V in ITER]            -- 2 loop vars -> pairs

ITER auto-wrapping inside comprehensions:

- 1 var, no "(" in ITER  -> ipairs(ITER)
- 2 vars, no "(" in ITER -> pairs(ITER)
- else passed through as-is

Misc

  • Strings/comments are hidden during substitution, so sigils inside them are safe: print("hi!") stays untouched.
  • Shebang #!... at top is rewritten to --....

PERFORMANCE

Runtime, default mode (depth=4, 16 trees built):

file       rows    fft.py   fft.lua  fft.luk (transpile+run)
--------   -----   ------   ------   -----------------------
auto93     398     0.080s   0.032s   0.039s
SS-N      53663    9.18s    6.24s    6.11s

Lua 1.5x-2.5x faster than Python. Transpile overhead ~7ms (constant, negligible on any real workload).

FILES

luk.lua      .luk -> .lua transpiler (filter)
lib.luk      "battery" helpers (argmin, sum, csv, of, ...)
fft.luk      example: multi-objective regression tree
Makefile     rule:  %.lua: %.luk luk.lua

VIM SUPPORT

syntax: http://tiny.cc/timm-lua  -> etc/syntax/luk.vim
nvim init at etc/nvimluk.lua.

SEE ALSO

fft.md                   help page for the fft.lua app
http://tiny.cc/fft       Python sibling project
http://tiny.cc/optimiz   example CSVs
http://tiny.cc/konfig    shared Makefile

LICENSE

MIT. (c) 2026 Tim Menzies.

AUTHOR

Tim Menzies <timm@ieee.org>
.-..-..-..-.
| || || |/ /
| `-' | (
`-----'-'`-' fewer ends, same lua
#!/usr/bin/env lua
-- fft.luk: fast-frugal multi-objective tree.
-- (c) 2026, Tim Menzies <timm@ieee.org>, MIT license
--
-- Options:
-- -s seed seed=1234567891
-- -p p distance exponent p=2
-- -b bins bins=7
-- -d depth depth=4
-- -R Round Round=2
-- -f file file=../optimiz/auto93.csv
local BIG = 1E32
local L = require("lib")
local abs, argmin, of, csv, sort = L.abs, L.argmin, L.of, L.csv, L.sort
local the = {seed=1234567891, p=2, bins=7, depth=4, Round=2,
file=(os.getenv("DOOT") or "..").."/optimiz/auto93.csv"}
-- forward decls (so closures can reference)
local add, adds, disty, distys, qty, grows, show
-- 1. Columns ------------------------------------------------
local Sym = function() return {symp=true, has={}} end
local Num = function(n, mu, m2)
return {nump=true, n=n or 0, mu=mu or 0, m2=m2 or 0} end
local sd = function(num)
if (num.n < 2) then return 0 end
return math.sqrt(math.max(0, num.m2) / (num.n - 1)) end
local welford = function(num, v, w)
local n, mu, m2 = num.n, num.mu, num.m2
n = n + w
if (n <= 0) then return Num() end
local d = v - mu
mu = mu + w * d / n
return Num(n, mu, m2 + w * d * (v - mu)) end
local norm = function(num, v)
local z = (v - num.mu) / (sd(num) + 1/BIG)
return 1 / (1 + math.exp(-1.7 * math.max(-3, math.min(3, z)))) end
local merge = function(i, j, w)
local w = w or 1
if (i.symp) then
local out = Sym()
for _, p in ipairs({{i,1}, {j,w}}) do
for k, vv in pairs(p[1].has) do
out.has[k] = (out.has[k] or 0) + p[2] * vv end end
return out end
local n = i.n + w * j.n
if (n <= 0) then return Num() end
local mu = (i.n*i.mu + w * j.n * j.mu) / n
local d = j.mu - i.mu
local m2 = i.m2 + w*j.m2 + w*d*d*i.n*j.n / n
return Num(n, mu, m2) end
-- 2. Data ---------------------------------------------------
local Data = function(src)
local it = setmetatable({names={}, klass=nil, x={}, y={},
goal={}, cols={}, rows={}}, DATA_MT)
local roles = function(names)
it.names = names
for at, s in ipairs(names) do
local z = s:sub(-1)
local first = s:sub(1,1)
if (first == first:lower()) then
it.cols[at] = Sym()
else
it.cols[at] = Num() end
if (z == "-" or z == "+" or z == "!") then
table.insert(it.y, at)
it.goal[at] = (z == "+") and 1 or 0
elseif (z ~= "X") then table.insert(it.x, at) end
if (z == "!") then it.klass = at end end end
roles(src[1])
for i = 2, #src do it = add(it, src[i]) end
return it end
add = function(it, v)
if (v == "?") then return it end
if (it.symp) then
it.has[v] = (it.has[v] or 0) + 1
elseif (it.nump) then
it = welford(it, v, 1)
else
table.insert(it.rows, v)
it.cols = (function() local _r={} for at, x in ipairs(v) do _r[#_r+1]=add(it.cols[at], x) end return _r end)() end
return it end
adds = function(src, it)
local it = it or Num()
for _, x in ipairs(src) do it = add(it, x) end
return it end
-- 3. Discretization ----------------------------------------
local cutsSyms = function(bins, tot, hi, at)
local out = {}
for k, l in pairs(bins) do
local score = l.m2 + merge(tot, l, -1).m2
out[#out+1] = {score=score, at=at, lo=hi[k], hi=hi[k], leaf=l} end
return out end
local cutsNums = function(bins, tot, hi, at)
local out = {}
local ks = sort((function() local _r={} for k, _ in pairs(bins) do _r[#_r+1]=k end return _r end)())
local l = Num()
for j = 1, #ks - 1 do
local k = ks[j]
l = merge(l, bins[k])
local score = l.m2 + merge(tot, l, -1).m2
out[#out+1] = {score=score, at=at, lo=-BIG, hi=hi[k], leaf=l} end
return out end
local cuts = function(data, rows, y)
local out, ys = {}, (function() local _r={} for _, r in ipairs(rows) do _r[#_r+1]=y(r) end return _r end)()
for _, at in ipairs(data.x) do
local c = data.cols[at]
local tot, bins, hi = Num(), {}, {}
for i, r in ipairs(rows) do
local v = r[at]
if (v ~= "?") then
local k = (c.symp and v)
or math.floor(the.bins * norm(c, v))
bins[k] = add(bins[k] or Num(), ys[i])
tot = add(tot, ys[i])
if (c.symp) then
hi[k] = v
else
hi[k] = math.max(hi[k] or -BIG, v) end end end
local inner = (c.symp and cutsSyms or cutsNums)(bins, tot, hi, at)
for _, x in ipairs(inner) do out[#out+1] = x end end
return out end
-- 4. Build a tree ------------------------------------------
disty = function(data, row)
local p, s = the.p, 0
for _, at in ipairs(data.y) do
s = s + abs(norm(data.cols[at], row[at]) - data.goal[at]) ^ p end
return (s / #data.y) ^ (1 / p) end
distys = function(data, rows)
local ys = (function() local _r={} for _, r in ipairs(rows) do _r[#_r+1]=disty(data, r) end return _r end)()
return adds(ys) end
local has = function(v, lo, hi) return v == "?" or (lo <= v and v <= hi) end
local rest = function(rows, at, lo, hi)
return (function() local _r={} for _, r in ipairs(rows) do if not has(r[at], lo, hi) then _r[#_r+1]=r end end return _r end)() end
grows = function(data, y, root, d)
local d = d or 0
local out = {}
if (d < the.depth) then
local floor = (#root.rows) ^ 0.33
local all = cuts(data, data.rows, y)
local cs = (function() local _r={} for _, c in ipairs(all) do if c.leaf.n > floor then _r[#_r+1]=c end end return _r end)()
if (#cs > 0) then
sort(cs, function(a,b) return a.leaf.mu < b.leaf.mu end)
for bit, c in ipairs({cs[1], cs[#cs]}) do
local no = rest(data.rows, c.at, c.lo, c.hi)
if (#no > 0) then
local child = {data.names}
for _, r in ipairs(no) do table.insert(child, r) end
for _, br in ipairs(grows(Data(child), y, root, d+1)) do
out[#out+1] = {bias=tostring(bit-1) .. br.bias,
tree={at=c.at, lo=c.lo, hi=c.hi,
left=c.leaf, right=br.tree}} end end end end end
if (#out == 0) then
local ys = (function() local _r={} for _, r in ipairs(data.rows) do _r[#_r+1]=y(r) end return _r end)()
out[#out+1] = {bias="", tree=adds(ys)} end
return out end
-- 5. Use a tree --------------------------------------------
local predict = function(t, row)
while (not t.nump) do
if (has(row[t.at], t.lo, t.hi)) then
t = t.left
else
t = t.right end end
return t.mu end
local tune = function(cands, rows, y)
local err = function(t)
local e = 0
for _, r in ipairs(rows) do
e = e + abs(y(r) - predict(t, r)) end
return e / #rows end
return argmin(cands, err) end
show = function(data, t)
if (t.nump) then
print(("%-33s leaf d2h %.2f n=%d"):fmt("", t.mu, t.n))
return nil end
local nm = data.names[t.at]
local c = ""
if (t.lo == t.hi) then
c = ("%s == %s"):fmt(nm, tostring(t.lo))
elseif (t.lo == -BIG) then
c = ("%s <= %s"):fmt(nm, tostring(qty(t.hi)))
else
c = ("%s >= %s"):fmt(nm, tostring(qty(t.lo))) end
local lf = t.left
print(("if %-30s then d2h %.2f n=%d"):fmt(c, lf.mu, lf.n))
show(data, t.right) end
-- 6. IO ----------------------------------------------------
qty = function(v)
if (type(v) == "number") then
if (math.floor(v) == v) then return math.floor(v) end
return tonumber(("%."..the.Round.."f"):fmt(v)) end
return v end
-- 7. Tests/demos -------------------------------------------
local test_main = function()
local data = Data(csv(the.file))
local y = function(r) return disty(data, r) end
local cands = {}
for _, b in ipairs(grows(data, y, data)) do
table.insert(cands, b.tree) end
show(data, tune(cands, data.rows, y)) end
local test_trees = function()
local data = Data(csv(the.file))
local y = function(r) return disty(data, r) end
local trees = grows(data, y, data)
for i, bt in ipairs(trees) do
local bias, t = bt.bias, bt.tree
local e = 0
for _, r in ipairs(data.rows) do
e = e + abs(y(r) - predict(t, r)) end
e = e / #data.rows
print(("===== tree %2d bias %-5s err %.3f ====="):fmt(
i, bias, e))
show(data, t)
print() end end
-- 8. Start-up ----------------------------------------------
for i = 1, #arg - 1 do
for k, _ in pairs(the) do
if (arg[i] == "-" .. k:sub(1,1)) then
the[k] = of(arg[i+1]) end end end
math.randomseed(the.seed)
local mode = "main"
for _, a in ipairs(arg) do
if (a == "--trees") then mode = "trees" end end
if (mode == "trees") then
test_trees()
else
test_main() end
#!/usr/bin/env lua
-- fft.luk: fast-frugal multi-objective tree.
-- (c) 2026, Tim Menzies <timm@ieee.org>, MIT license
--
-- Options:
-- -s seed seed=1234567891
-- -p p distance exponent p=2
-- -b bins bins=7
-- -d depth depth=4
-- -R Round Round=2
-- -f file file=../optimiz/auto93.csv
BIG := 1E32
L := require("lib")
abs, argmin, of, csv, sort := L.abs, L.argmin, L.of, L.csv, L.sort
the := {seed=1234567891, p=2, bins=7, depth=4, Round=2,
file=(os.getenv("DOOT") or "..").."/optimiz/auto93.csv"}
-- forward decls (so closures can reference)
local add, adds, disty, distys, qty, grows, show
-- 1. Columns ------------------------------------------------
Sym := fun(): !{symp=true, has={}}
Num := fun(n, mu, m2):
!{nump=true, n=n or 0, mu=mu or 0, m2=m2 or 0}
sd := fun(num):
if (num.n < 2): !0
!math.sqrt(math.max(0, num.m2) / (num.n - 1))
welford := fun(num, v, w):
n, mu, m2 := num.n, num.mu, num.m2
n += w
if (n <= 0): !Num()
d := v - mu
mu += w * d / n
!Num(n, mu, m2 + w * d * (v - mu))
norm := fun(num, v):
z := (v - num.mu) / (sd(num) + 1/BIG)
!1 / (1 + math.exp(-1.7 * math.max(-3, math.min(3, z))))
merge := fun(i, j, w):
w := w or 1
if (i.symp):
out := Sym()
for _, p in ipairs({{i,1}, {j,w}}):
for k, vv in pairs(p[1].has):
out.has[k] = (out.has[k] or 0) + p[2] * vv
!out
n := i.n + w * j.n
if (n <= 0): !Num()
mu := (i.n*i.mu + w * j.n * j.mu) / n
d := j.mu - i.mu
m2 := i.m2 + w*j.m2 + w*d*d*i.n*j.n / n
!Num(n, mu, m2)
-- 2. Data ---------------------------------------------------
Data := fun(src):
it := setmetatable({names={}, klass=nil, x={}, y={},
goal={}, cols={}, rows={}}, DATA_MT)
roles := fun(names):
it.names = names
for at, s in ipairs(names):
z := s:sub(-1)
first := s:sub(1,1)
if (first == first:lower()):
it.cols[at] = Sym()
else:
it.cols[at] = Num()
if (z == "-" or z == "+" or z == "!"):
table.insert(it.y, at)
it.goal[at] = (z == "+") and 1 or 0
elseif (z ~= "X"): table.insert(it.x, at)
if (z == "!"): it.klass = at
roles(src[1])
for i = 2, #src: it = add(it, src[i])
!it
add = fun(it, v):
if (v == "?"): !it
if (it.symp):
it.has[v] = (it.has[v] or 0) + 1
elseif (it.nump):
it = welford(it, v, 1)
else:
table.insert(it.rows, v)
it.cols = [add(it.cols[at], x) for at, x in ipairs(v)]
!it
adds = fun(src, it):
it := it or Num()
for _, x in ipairs(src): it = add(it, x)
!it
-- 3. Discretization ----------------------------------------
cutsSyms := fun(bins, tot, hi, at):
out := {}
for k, l in pairs(bins):
score := l.m2 + merge(tot, l, -1).m2
out[#out+1] = {score=score, at=at, lo=hi[k], hi=hi[k], leaf=l}
!out
cutsNums := fun(bins, tot, hi, at):
out := {}
ks := sort([k for k, _ in pairs(bins)])
l := Num()
for j = 1, #ks - 1:
k := ks[j]
l = merge(l, bins[k])
score := l.m2 + merge(tot, l, -1).m2
out[#out+1] = {score=score, at=at, lo=-BIG, hi=hi[k], leaf=l}
!out
cuts := fun(data, rows, y):
out, ys := {}, [y(r) for _, r in ipairs(rows)]
for _, at in ipairs(data.x):
c := data.cols[at]
tot, bins, hi := Num(), {}, {}
for i, r in ipairs(rows):
v := r[at]
if (v ~= "?"):
k := (c.symp and v)
or math.floor(the.bins * norm(c, v))
bins[k] = add(bins[k] or Num(), ys[i])
tot = add(tot, ys[i])
if (c.symp):
hi[k] = v
else:
hi[k] = math.max(hi[k] or -BIG, v)
inner := (c.symp and cutsSyms or cutsNums)(bins, tot, hi, at)
for _, x in ipairs(inner): out[#out+1] = x
!out
-- 4. Build a tree ------------------------------------------
disty = fun(data, row):
p, s := the.p, 0
for _, at in ipairs(data.y):
s += abs(norm(data.cols[at], row[at]) - data.goal[at]) ^ p
!(s / #data.y) ^ (1 / p)
distys = fun(data, rows):
ys := [disty(data, r) for _, r in ipairs(rows)]
!adds(ys)
has := fun(v, lo, hi): !v == "?" or (lo <= v and v <= hi)
rest := fun(rows, at, lo, hi):
![r for _, r in ipairs(rows) if not has(r[at], lo, hi)]
grows = fun(data, y, root, d):
d := d or 0
out := {}
if (d < the.depth):
floor := (#root.rows) ^ 0.33
all := cuts(data, data.rows, y)
cs := [c for _, c in ipairs(all) if c.leaf.n > floor]
if (#cs > 0):
sort(cs, fun(a,b): !a.leaf.mu < b.leaf.mu end)
for bit, c in ipairs({cs[1], cs[#cs]}):
no := rest(data.rows, c.at, c.lo, c.hi)
if (#no > 0):
child := {data.names}
for _, r in ipairs(no): table.insert(child, r)
for _, br in ipairs(grows(Data(child), y, root, d+1)):
out[#out+1] = {bias=tostring(bit-1) .. br.bias,
tree={at=c.at, lo=c.lo, hi=c.hi,
left=c.leaf, right=br.tree}}
if (#out == 0):
ys := [y(r) for _, r in ipairs(data.rows)]
out[#out+1] = {bias="", tree=adds(ys)}
!out
-- 5. Use a tree --------------------------------------------
predict := fun(t, row):
while (not t.nump):
if (has(row[t.at], t.lo, t.hi)):
t = t.left
else:
t = t.right
!t.mu
tune := fun(cands, rows, y):
err := fun(t):
e := 0
for _, r in ipairs(rows):
e += abs(y(r) - predict(t, r))
!e / #rows
!argmin(cands, err)
show = fun(data, t):
if (t.nump):
print(("%-33s leaf d2h %.2f n=%d"):fmt("", t.mu, t.n))
!nil
nm := data.names[t.at]
c := ""
if (t.lo == t.hi):
c = ("%s == %s"):fmt(nm, tostring(t.lo))
elseif (t.lo == -BIG):
c = ("%s <= %s"):fmt(nm, tostring(qty(t.hi)))
else:
c = ("%s >= %s"):fmt(nm, tostring(qty(t.lo)))
lf := t.left
print(("if %-30s then d2h %.2f n=%d"):fmt(c, lf.mu, lf.n))
show(data, t.right)
-- 6. IO ----------------------------------------------------
qty = fun(v):
if (type(v) == "number"):
if (math.floor(v) == v): !math.floor(v)
!tonumber(("%."..the.Round.."f"):fmt(v))
!v
-- 7. Tests/demos -------------------------------------------
test_main := fun():
data := Data(csv(the.file))
y := fun(r): !disty(data, r) end
cands := {}
for _, b in ipairs(grows(data, y, data)):
table.insert(cands, b.tree)
show(data, tune(cands, data.rows, y))
test_trees := fun():
data := Data(csv(the.file))
y := fun(r): !disty(data, r) end
trees := grows(data, y, data)
for i, bt in ipairs(trees):
bias, t := bt.bias, bt.tree
e := 0
for _, r in ipairs(data.rows):
e += abs(y(r) - predict(t, r))
e = e / #data.rows
print(("===== tree %2d bias %-5s err %.3f ====="):fmt(
i, bias, e))
show(data, t)
print()
-- 8. Start-up ----------------------------------------------
for i = 1, #arg - 1:
for k, _ in pairs(the):
if (arg[i] == "-" .. k:sub(1,1)):
the[k] = of(arg[i+1])
math.randomseed(the.seed)
mode := "main"
for _, a in ipairs(arg):
if (a == "--trees"): mode = "trees"
if (mode == "trees"):
test_trees()
else:
test_main()

fft - fast-frugal multi-objective tree

Smallest useful AI/XAI optimization tool. Builds a tiny regression tree from CSV via greedy min-variance cuts on incremental Welford μ/σ stats. Written in .luk (see ,luk.md for the language).

git clone http://tiny.cc/optimiz && git clone http://tiny.cc/luk
cd luk && make fft.lua lib.lua
lua fft.lua -f ../optimiz/auto93.csv

NAME

fft - fast-frugal multi-objective regression tree

SYNOPSIS

make fft.lua lib.lua                    # transpile
lua fft.lua [-flag VAL]... [--TEST]     # run

OPTIONS

-b bins     numeric bin count            (7)
-d depth    max tree depth               (4)
-s seed     random seed                  (1234567891)
-p p        distance exponent            (2)
-R Round    display decimals             (2)
-f file     data file                    (../optimiz/auto93.csv)

CLI overrides match by first letter of each key in the table.

DATA

CSV with header row. Column-name first/last char encodes:

  first char UPPER  -> numeric column (Num)
  first char lower  -> symbolic column (Sym)
  suffix '+'        -> numeric goal, maximize
  suffix '-'        -> numeric goal, minimize
  suffix '!'        -> symbolic goal (klass)
  suffix 'X'        -> ignore
  else              -> predictor

Missing values: '?'.

Example header:

  Clndrs,Volume,HpX,Model,origin,Lbs-,Acc+,Mpg+

MODES

(default)    train on -f, tune, show best tree
--trees      enumerate all candidate trees + err

TREE OUTPUT

Each non-leaf line:

  if <col OP val>      then d2h <mean> n=<count>

Leaf line:

  (indent)             leaf  d2h <mean> n=<count>

d2h = distance to heaven (lower = better).

INTERNALS

Sym  = {symp=true, has={}}             -- value -> count
Num  = {nump=true, n, mu, m2}          -- Welford running stats
Data = {names, x, y, goal, cols, rows} -- no metatable

Helpers in lib.luk:
  argmin, argmax, sum, mean, sort, keys
  of (string -> number/bool/string)
  csv (file -> list of rows)
  abs, max, min, sqrt, exp, floor (math aliases)

EXIT

0  success
>0 Lua error (transpile or runtime)

SEE ALSO

,luk.md                .luk language reference
http://tiny.cc/fft       Python sibling
http://tiny.cc/optimiz   example CSVs

LICENSE

MIT. (c) 2026 Tim Menzies.

AUTHOR

Tim Menzies <timm@ieee.org>
-- lib.luk: Lua "battery" for .luk programs.
-- usage: L := require("lib")
-- abs, argmin = L.abs, L.argmin -- or use L.abs etc.
-- adds :fmt(...) method to all strings (alias for string.format)
getmetatable("").__index.fmt = string.format
local abs = math.abs
local max = math.max
local min = math.min
local sqrt = math.sqrt
local exp = math.exp
local floor = math.floor
local sum -- forward: mean uses sum
local sort = function(t, f) table.sort(t, f); return t end
local of = function(z) return z=="True" or z~="False" and (tonumber(z) or z) end
local keys = function(t) return sort((function() local _r={} for k, _ in pairs(t) do _r[#_r+1]=k end return _r end)()) end
local mean = function(xs) return sum(xs) / #xs end
sum = function(xs)
local s = 0
for _, x in ipairs(xs) do
s = s + x end
return s end
local argmin = function(xs, key, cmp)
cmp = cmp or function(a,b) return a<b end
local best = xs[1]
local bv = key(best)
for i = 2, #xs do
local v = key(xs[i])
if (cmp(v, bv)) then best, bv = xs[i], v end end
return best end
local argmax = function(xs, key)
return argmin(xs, key, function(a,b) return a>b end) end
local csv = function(file)
local out = {}
for ln in io.lines(file) do
local ln = ln:gsub("^%s+", ""):gsub("%s+$", "")
if (#ln > 0 and ln:sub(1,1) ~= "#") then
local row = {}
for x in ln:gmatch("[^,]+") do row[#row+1] = of(x) end
out[#out+1] = row end end
return out end
return {abs=abs, max=max, min=min, sqrt=sqrt, exp=exp, floor=floor,
argmin=argmin, argmax=argmax, sum=sum, mean=mean,
sort=sort, keys=keys, of=of, csv=csv}
-- lib.luk: Lua "battery" for .luk programs.
-- usage: L := require("lib")
-- abs, argmin = L.abs, L.argmin -- or use L.abs etc.
-- adds :fmt(...) method to all strings (alias for string.format)
getmetatable("").__index.fmt = string.format
abs := math.abs
max := math.max
min := math.min
sqrt := math.sqrt
exp := math.exp
floor := math.floor
local sum -- forward: mean uses sum
sort := fun(t, f): table.sort(t, f); !t
of := fun(z): !z=="True" or z~="False" and (tonumber(z) or z)
keys := fun(t): !sort([k for k, _ in pairs(t)])
mean := fun(xs): !sum(xs) / #xs
sum = fun(xs):
s := 0
for _, x in ipairs(xs):
s += x
!s
argmin := fun(xs, key, cmp):
cmp = cmp or fun(a,b): !a<b end
best := xs[1]
bv := key(best)
for i = 2, #xs:
v := key(xs[i])
if (cmp(v, bv)): best, bv = xs[i], v
!best
argmax := fun(xs, key):
!argmin(xs, key, fun(a,b): !a>b end)
csv := fun(file):
out := {}
for ln in io.lines(file):
ln := ln:gsub("^%s+", ""):gsub("%s+$", "")
if (#ln > 0 and ln:sub(1,1) ~= "#"):
row := {}
for x in ln:gmatch("[^,]+"): row[#row+1] = of(x)
out[#out+1] = row
!out
!{abs=abs, max=max, min=min, sqrt=sqrt, exp=exp, floor=floor,
argmin=argmin, argmax=argmax, sum=sum, mean=mean,
sort=sort, keys=keys, of=of, csv=csv}
# Lua.ssh --- Sheet definitions for Lua source code
# Copyright (c) 2014 Kenji Rikitake
# Copyright (c) 1999 Edward Arthur, Akim Demaille, Miguel Santana
#
#
# This file is NOT a part of a2ps.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
## This style is derived from Edward Arthur's AWK Style Sheet
style Lua is
written by "Kenji Rikitake <kenji.rikitake@acm.org>"
version is 0.1
requires a2ps version 4.9.7
documentation is
"This style file is intended to support the Lua programming language source code."
end documentation
alphabets are
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0"
case sensitive
keywords in Keyword are
and, false, ipairs, nil, not, or, pairs, rawequal, rawget,
rawlen, rawset, select, tonumber, tostring, true, type
end keywords
#keywords in Keyword_strong are
keywords in Keyword_strong are
assert, break, collectgarbage, do, dofile, else, elseif, "end",
error, for, function, getmetatable, goto, if, "in", local,
load, loadfile, next, pcall, print, repeat, require, return,
setmetatable, then, until, while, xpcall, _G, _VERSION
end keywords
keywords in Label_strong are
"^" function
end keywords
keywords in Comment are
self
end keywords
sequences are
"[[" Comment "]]",
"--" Comment,
C-string
end sequences
end style
package = "luk"
version = "0.1-1"
source = {
url = "git+https://tiny.cc/luk",
}
description = {
summary = "tiny .luk -> Lua transpiler (~100-line module)",
detailed = [[
luk is the .luk language: a tiny indentation-based dialect
that transpiles to Lua. Same Lua semantics, fewer `end`s,
Python-style list comprehensions.
The module returns a single function:
local luk = require("luk")
local lua_src = luk(fun_src)
Worked example (fft.luk, a multi-objective regression tree)
at http://tiny.cc/luk.
]],
license = "MIT",
homepage = "http://tiny.cc/luk",
maintainer = "Tim Menzies <timm@ieee.org>",
}
dependencies = { "lua >= 5.3" }
build = {
type = "builtin",
modules = { luk = "luk.lua" },
install = { conf = { ",luk.md", "luk.vim" } },
}
-- luk.lua : ".luk" -> Lua transpiler. Returns transpile fn.
-- fun=function !=return NAME:=V -> local NAME=V
-- if (c): elseif (c): else: for X in Y: while c: fun(a):
-- Body after ":" = one-liner (auto end). Indent block ends on outdent.
-- [e for v in xs] / [e for v in xs if c] = comprehension.
local function comprehension(e,v,i,c)
if not v:find"," and not i:find"%(" then
v,i = "_,"..v, "ipairs("..i..")"
elseif not i:find"%(" then i = "pairs("..i..")" end
local g = c and ("if "..c.." then ") or ""
local z = c and "end " or ""
return ("(function() local _r={} for %s in %s "..
"do %s_r[#_r+1]=%s %send return _r end)()"
):format(v,i,g,e,z) end
local function oneLiner(r)
if not r:find":%s+%S" or r:find"%f[%w_]end%f[%W]" then
return false end
local t = r:gsub("^%s+","")
return t:match"^if%s*%(" or t:match"^elseif%s*%("
or t:match"^else%s*:" or t:match"^for%s"
or t:match"^while%s" or t:match":=%s*fun%s*%("
or t:match"^[%w_.%[%]\"'%-]+%s*=%s*fun%s*%(" end
local function opensBlock(s)
if s:match"%f[%w_]then%s*$" or s:match"%f[%w_]do%s*$" then
return true end
local t = s:gsub("^%s+","")
return t:match"^local%s+[%w_.,%s]+%s*=%s*function%b()%s*$"
or t:match"^[%w_.%[%]\"'%-]+%s*=%s*function%b()%s*$"
or t:match"^return%s+function%b()%s*$" end
local function cont(r) return r:match"^%s*else" end
local function ind(s) return #(s:match"^%s*":gsub("\t"," ")) end
local function line(b)
if b:match"^%s*%-%-" or b:match"^%s*$" or b:match"^#!" then
return b end
local s, c = {}, ""
local function hide(m) s[#s+1]=m; return "\3"..#s.."\3" end
local R = {
{'%[%[.-%]%]', hide},
{'"[^"]*"', hide},
{"'[^']*'", hide},
{"(%s*%-%-.*)$", function(x) c=x; return "" end},
{"^(%s*)([%w_][%w_,%s]*)%s*:=", "%1local %2 ="},
{"^(%s*)([%w_.]+)%s*([%+%-%*/])=%s+",
"%1%2 = %2 %3 "},
{"%f[%w_]fun%f[%W]", "function"},
{"%f[%w_]if%s+(.+)%s*:%s*$", "if %1 then"},
{"%f[%w_]if%s+(.+)%s*:(%s)", "if %1 then%2"},
{"%f[%w_]elseif%s+(.+)%s*:%s*$", "elseif %1 then"},
{"%f[%w_]elseif%s+(.+)%s*:(%s)", "elseif %1 then%2"},
{"(%f[%w_]for%s.+)%s*:%s*$", "%1 do"},
{"(%f[%w_]for%s.+)%s*:(%s)", "%1 do%2"},
{"(%f[%w_]while%s.+)%s*:%s*$", "%1 do"},
{"(%f[%w_]while%s.+)%s*:(%s)", "%1 do%2"},
{"function%s*(%b())%s*:%s*$", "function%1"},
{"function%s*(%b())%s*:(%s)", "function%1%2"},
{"(%f[%w_]else)%s*:%s*$", "%1"},
{"(%f[%w_]else)%s*:(%s)", "%1%2"},
{"!=", "~="},
{"!%s*", "return "},
{"%b[]", function(m)
local n = m:sub(2,-2)
local e,v,i,k = n:match"^(.-) for (.-) in (.-) if (.+)$"
if e then return comprehension(e,v,i,k) end
e,v,i = n:match"^(.-) for (.-) in (.+)$"
if e then return comprehension(e,v,i) end
return m end},
{"\3(%d+)\3", function(n) return s[tonumber(n)] end},
}
for _,p in ipairs(R) do b = b:gsub(p[1], p[2]) end
return b..c end
return function(src)
local out, stk = {}, {}
local function close(i)
while #stk>0 and stk[#stk] >= i do
stk[#stk] = nil
local k = #out
while k>0 and (out[k]:match"^%s*$"
or out[k]:match"^%s*%-%-") do k = k-1 end
if k>0 then out[k] = out[k] .. " end"
else out[#out+1] = "end" end end end
for r0 in (src.."\n"):gmatch"([^\n]*)\n" do
local r = r0
if r:match"^%s*$" then out[#out+1] = r
elseif r:match"^%s*%-%-" then close(ind(r)); out[#out+1] = r
else
local i, c = ind(r), cont(r)
close(c and i+1 or i)
if oneLiner(r) and not c and not r:match"%f[%w_]end%s*$" then
r = r .. " end" end
r = line(r)
out[#out+1] = r
if opensBlock(r) and not c then stk[#stk+1] = i end end end
close(-1)
return table.concat(out, "\n") end
# luk.rc - sourced after $(KONFIG)/bashrc by `make sh`.
# Overrides the konfig `vi` alias with a function that adds
# the .luk syntax overlay and accepts positional args.
FUN_DIR="${FUN_DIR:-$(pwd)}"
# Build a vim runtime path so vim natively loads syntax/luk.vim
# whenever a buffer's filetype becomes "luk" (handles :e other.luk).
luk_rtp() {
local rtp="${TMPDIR:-/tmp}/luk-rtp-$$"
mkdir -p "$rtp/syntax" "$rtp/ftdetect"
ln -sf "$FUN_DIR/luk.vim" "$rtp/syntax/luk.vim"
# force ft=luk (override shebang-based ft=lua detection)
printf 'au BufRead,BufNewFile *.luk setlocal ft=luk\n' \
> "$rtp/ftdetect/luk.vim"
echo "$rtp"
}
unalias vi 2>/dev/null
vi() {
local init=""
[ -f "${KONFIG:-.}/init.lua" ] && init="-u ${KONFIG}/init.lua"
local rtp=$(luk_rtp)
NVIM_APPNAME="${APP:-luk}" nvim --clean $init \
-c "set rtp^=$rtp" \
-c "syntax on" \
-c "filetype plugin indent on" \
-c "runtime! ftdetect/*.vim" \
"$@"
}
# transpile any .luk file via luk.lua
fun2lua() { lua -e 'io.write(require("luk")(io.read("*a")))' < "$1"; }
" syntax/luk.vim : syntax for the "fun" language.
" Reuses Lua syntax then overlays fun-specific tokens.
if exists("b:current_syntax") | finish | endif
runtime! syntax/lua.vim
unlet! b:current_syntax
syntax clear luaError
" `fun ... end` block (mirrors lua's luaFunctionBlock).
syntax region funFunBlock transparent matchgroup=funKeyword
\ start=/\<fun\>/ end=/\<end\>/ contains=TOP
" `if (cond) ... end` block.
syntax region funIfBlock transparent matchgroup=funKeyword
\ start=/\<if\>\ze\s*(/ end=/\<end\>/ contains=TOP
syntax keyword funKeyword let else elseif
syntax match funReturn /!/
syntax match funDeclare /:=/
syntax match funBlockCol /:\s*$/
syntax match funBlockCol /:\s\+/
highlight default link funKeyword Keyword
highlight default link funReturn Special
highlight default link funDeclare Operator
highlight default link funBlockCol Operator
let b:current_syntax = "fun"
# vim: ts=2 sw=2 sts=2 et :
# knobs only; shared targets live in $(KONFIG)/Makefile
KONFIG ?= ../konfig
APP := luk
MAIN := fft.luk
EXT := luk
LANG := lua
LINT := true
TOOLS := lua:run-lua
PKG := lua gawk neovim tmux
$(KONFIG)/Makefile:
@test -f $@ || { echo "missing konfig: git clone http://tiny.cc/konfig $(KONFIG)"; exit 1; }
include $(KONFIG)/Makefile
# ---- transpile rule -----------------------------------------------
# .luk -> .lua via luk.lua library (returns transpile function)
%.lua: %.luk luk.lua
lua -e 'io.write(require("luk")(io.read("*a")))' < $< > $@
# ---- luk shell: konfig bashrc + luk.rc (vi w/ .luk mode) ------
fsh: ## luk tuned bash (konfig bashrc + luk.rc overlay)
$(call need,nvim,fsh)
$(call need,git,fsh)
$(call konfig)
@KONFIG=$(abspath $(KONFIG)) APP=$(APP) MAIN=$(MAIN) BANNER=$(abspath $(BANNER)) \
bash --rcfile <(cat $(KONFIG)/bashrc luk.rc) -i
# ---- pdf: override konfig's rule, use full path to lua.ssh --------
# Works under GNU Make 3.81 (macOS default) which lacks $(file ...).
LUK_SSH ?= lua.ssh
Cols ?= 2
Font ?= 9
Orient ?= landscape
$(HOME)/tmp/%.pdf: %.luk
@mkdir -p $(HOME)/tmp
@echo "pdfing : $@ ..."
@a2ps -Bj --$(Orient) --line-numbers=1 --highlight-level=heavy \
--borders=no --pro=color \
--left-footer="" --right-footer="" --footer="page %p." \
--pretty-print=$(LUK_SSH) -M letter \
--font-size=$(Font) --columns=$(Cols) \
-o - $< 2> >(grep -v '^a2ps:/' >&2) \
| ps2pdf - $@
@echo "wrote $@"
@open $@

TODO

PDF printing broken

make ~/tmp/fft.pdf falls back to plain style — no Lua syntax color.

Root cause: GNU Make 3.81 (macOS default) lacks $(file < FILE) (added in Make 4.0). The export LUASSH := $(file < lua.ssh) returns empty, so konfig's pdf rule writes an empty lua.ssh and a2ps errors out.

Fixes (pick one):

  • Install newer Make (brew install make, use gmake).

  • Read lua.ssh in the recipe via shell, not Make's $(file ...):

    $(HOME)/tmp/%.pdf: %.luk
        LUASSH="$$(cat $(LUK_SSH))" $(MAKE) -f $(KONFIG)/Makefile $@
    
  • Symlink/copy lua.ssh system-wide:

    sudo cp $(HOME)/gits/timm/lua/etc/lua.ssh \
      /opt/homebrew/opt/a2ps/share/a2ps/sheets/
    

,luk.md doc audit

  • tiny.cc/fun -> tiny.cc/luk URLs
  • "fun -" title -> "luk -"
  • verify SYNOPSIS code blocks all use luk (not funny)
  • VIM SUPPORT section: confirm luk.vim path is correct

a2ps rule cleanup

  • Konfig's ~/tmp/%.pdf rule needs EXT=luk + LANG=lua AND a populated SSH=VARNAME where the env var contains the .ssh body. Currently failing — see above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment