- Lua 76.5%
- C 13.7%
- Shell 6.1%
- Makefile 2.2%
- Vim Script 1.5%
brazilian-software-engineering
tif is a small interpreted programming language implemented in Lua.
It was designed to be simple, lightweight and easy to understand.
Overview
The language is built around a classic interpreter architecture:
- Lexer – converts source code into tokens, with line and column tracking
- Parser – builds an Abstract Syntax Tree (AST) using Pratt parsing for correct operator precedence
- Interpreter – evaluates the AST directly with scoped environments and runtime safety limits
tif supports both Lua 5.4 and LuaJIT as host runtimes. The optimized default build targets LuaJIT, while Lua 5.4 remains available as a compatibility/runtime choice.
Stable release: v0.3-saren
This release extends the language with functional programming capabilities and performance improvements. These features enable higher-order function patterns and more idiomatic functional code while maintaining backward compatibility with v0.2.
Highlights
- Lambda expressions — anonymous functions as first-class values
- Syntax:
fn (params) { body },fn :: return_type (params) { body }orfn (params) -> return_type { body } - Enable functional patterns:
map,filter,reducewith closures
- Syntax:
- Typed lambda returns — optional type annotations for lambda functions
- Similar to named functions:
fn :: int (x) { return x * 2 } - Support for typed arrays:
fn :: <int>[] (n) { ... } - Runtime type checking on return values
- Similar to named functions:
- Closures — lambdas capture their defining environment
- Variables from enclosing scopes are accessible within lambda body
- Enables currying and higher-order function composition
- First-class functions — lambdas and named functions can be assigned to variables
- Pass functions as arguments to other functions
- Store functions in data structures
- Expression-based lambda invocation — call lambdas inline
- Direct syntax:
(lambda_expr)(args)for immediate application
- Direct syntax:
- Improved module loading — robust
thd.sosearch paths- Automatic detection from standard installation locations
- Fallback to
package.loadlibwith full path resolution - Clearer error diagnostics for missing extensions
- Performance optimizations — reduced interpretation overhead
- Reduced interpreter overhead in module loading, scoping and expression handling
- Efficient scoping and variable resolution
- Runtime type safety for lambdas — enforce type contracts
- Parameter and return type validation where specified
- Safe interop with typed code
- Pattern matching — classic
match / case {}for simple value matching, and the ccl-style (complex conditional logic)match { pattern => expr }for ranges, alternatives, captures and guards - Portable binaries with
-p— package source into a.ptfbinary with embedded resolved AST for faster startup across tif and lusotif runtimes
Examples
Simple lambda:
letdouble=fn (x){returnx*2}print(double(5))// 10
Typed lambda:
letsquare=fn :: int(int:: x){returnx*x}letresult=square(7)// type-safe result
Explicit return lambda:
letincrement=fn (int:: x)-> void{returnn+1}print(increment(5))Higher-order function:
letadder=fn :: int(int:: a){returnfn :: int(int:: b){returna+b}}letadd5=adder(5)print(add5(3))// 8
Passing lambdas as arguments (pattern for functional utilities):
letapply=fn :: int(int:: x,fn_obj){returnfn_obj(x)}letdouble=fn (int:: n){returnn*2}print(apply(5,double))// 10
Optional typed function parameters:
fn range(?int:: from,int:: to)-> <int>[]{// if omitted, `from` receives the type default (`0` for int)
}Typed string interpolation:
letraw="12"print("value as int: #(int){raw}")print("classic mode: #{raw}")Previous release: v0.2-praven
This release introduces major improvements over the previous v0.1.x-cute, expanding the language with modules, arrays, pointers, structured data, and a full standard library.
Highlights
- Module system —
require "file"with caching and circular import detection - Global modules —
require <mod>loads C-based stdlib modules pubkeyword — explicit export control for modules- Typed arrays —
<int>,<string>, etc., with bounds checking and enforced element types - Array utilities — built-ins such as
new_array,len,push,pop forloops — range-based (1..n) and array iteration- String interpolation —
"hello #{name}"with inline expressions - Pointers —
&x,*px, powered by THD with real memory backing - Structs — user-defined data types with named fields for structured data modeling
- Typed function returns — including typed array returns (
fn <int>[]::name()) - Uninitialized typed variables — safe defaults
- Improved error messages — include file, line and column with precise diagnostics
- CLI improvements — module search path via
-I - Standard library — modular C-based stdlib (
std,stdr/math,stdr/io,stdr/os) - Pattern matching — classic
casematching and ccl-style=>matching - File extension —
.tif
THD — Tif Heap Dialoguer
THD is the low-level C extension that gives tif real memory backing for pointer operations. It is compiled as a shared library (thd.so) and loaded by the interpreter at runtime via Lua's C module system.
Architecture
tif source code
│
▼
interpreter.lua
│ &x — allocates a cell on the THD heap
│ *px — reads from the C cell
│ *px = v — writes to the C cell AND syncs back to the Lua variable
▼
thd.so (Lua C extension)
│
▼
C heap arena
┌─────────────────────────────────────┐
│ 65 536 cells ×ばつ tagged value │
│ 16 MB raw byte pool │
└─────────────────────────────────────┘
How it works
The THD manages two memory regions in a single C process:
Cell arena — an array of 65 536 fixed-size cells. Each cell holds a tagged union:
| Tag | C type | Tif type |
|---|---|---|
TAG_INT |
int64_t |
int |
TAG_FLOAT |
double |
float |
TAG_BOOL |
int32_t |
bool |
TAG_BYTES |
uint8_t * |
raw buffer |
Every cell carries a magic stamp (0x54494621 — "TIF!") validated on every read and write. A freed cell has its magic zeroed, so use-after-free and double-free are caught immediately.
Byte pool — a 16 MB bump allocator for TAG_BYTES cells. Raw byte buffers are allocated by advancing a single offset pointer. There is intentionally no free for individual byte allocations — the pool is reset when the interpreter resets.
Pointer lifecycle
let int :: x = 5
let *int :: px = &x
&xcallsalloc_int(5)on the THD — allocates a cell, returns its slot index as a handle- The interpreter creates a pointer object
{ handle=42, base_type="int", env=<scope>, name="x" } - The scope reference (
env+name) is stored alongside the handle for write-back sync
*px = 10
mem().write(42, 10)— writes10into the C cell at slot 42scope["x"] = 10— syncs the value back to the original tif variable
This bidirectional sync ensures that both print(*px) (reads from C) and print(x) (reads from Lua scope) always agree.
Safety
- Invalid handle — every operation validates the magic stamp and checks the slot index bounds before accessing the cell
- Double-free — freeing a cell zeroes the magic; a subsequent access on the same handle raises an error
- Type enforcement —
writechecks that the value type matches the cell tag - Byte offset bounds —
read_byteandwrite_bytevalidate the offset against the cell size
Exposed API
THD exposes the following functions to Lua (used internally by the interpreter):
| Function | Description |
|---|---|
alloc_int(v) |
allocate an int64 cell, returns handle |
alloc_float(v) |
allocate a double cell, returns handle |
alloc_bool(v) |
allocate a bool cell, returns handle |
alloc_bytes(n) |
allocate n bytes in the pool, returns handle |
free(handle) |
mark a cell as free |
read(handle) |
read the value from a cell |
write(handle, v) |
write a value to a cell (type-checked) |
read_byte(handle, i) |
read byte at 1-based offset i from a bytes cell |
write_byte(handle, i, b) |
write byte at 1-based offset i |
tag(handle) |
returns the type tag string |
size(handle) |
returns byte count for bytes cells |
addr(handle) |
returns the real RAM address of the cell as an integer |
stats() |
returns { allocs, frees, live, bytes_used, bytes_total } |
Compiling THD
gcc -shared -fPIC -O2 -o thd.so thd.c $(pkg-config --cflags --libs lua5.4)
Place thd.so next to interpreter.lua (for source runs) or in /usr/local/lib/tif/ (for installed runs). The installer handles this automatically.
For LuaJIT builds, compile against LuaJIT instead of lua5.4:
gcc -shared -fPIC -O2 -o thd.so thd.c -I/usr/include/luajit-2.1 -L/usr/lib/x86_64-linux-gnu -lluajit-5.1
Standard library
The tif stdlib is a set of C shared libraries installed under /usr/local/lib/tif/stdlib/. Each module is a standard Lua C extension loaded transparently by the interpreter when require <mod> is used.
Installed layout
/usr/local/lib/tif/
├── *.lua
├── *.luac (Lua 5.4 installs)
├── thd.so
└── stdlib/
├── std.so ← require <std>
└── stdr/
├── math.so ← require <stdr/math>
├── io.so ← require <stdr/io>
└── os.so ← require <stdr/os>
require <std> — core utilities
| Function | Description |
|---|---|
std.println(v) |
print value followed by newline |
std.print(v) |
print value without newline |
std.eprintln(v) |
print to stderr with newline |
std.eprint(v) |
print to stderr without newline |
std.tostring(v) |
convert any value to string |
std.toint(v) |
convert string or float to int |
std.tofloat(v) |
convert string or int to float |
std.tobool(v) |
convert value to bool |
std.type(v) |
returns type name as string |
std.assert(cond, msg) |
abort with message if cond is false |
std.panic(msg) |
unconditional abort |
std.input(prompt?) |
print an optional prompt and read one line from stdin |
std.is_nil(v) |
returns true if v is nil |
require <stdr/math> — mathematics
| Function | Description |
|---|---|
math.sqrt(x) |
square root |
math.pow(x, y) |
x to the power of y |
math.abs(x) |
absolute value |
math.floor(x) |
round down |
math.ceil(x) |
round up |
math.round(x) |
round to nearest |
math.min(...) |
minimum of arguments |
math.max(...) |
maximum of arguments |
math.clamp(v, lo, hi) |
clamp value to range |
math.sin(x) |
sine (radians) |
math.cos(x) |
cosine (radians) |
math.tan(x) |
tangent (radians) |
math.asin(x) |
arc sine |
math.acos(x) |
arc cosine |
math.atan(x) |
arc tangent |
math.deg(x) |
radians to degrees |
math.rad(x) |
degrees to radians |
math.log(x) |
natural logarithm |
math.log(x, base) |
logarithm with base |
math.exp(x) |
e^x |
math.sign(x) |
-1, 0 or 1 |
math.factorial(n) |
n! (max 20) |
math.gcd(a, b) |
greatest common divisor |
math.is_int(x) |
true if x has no fractional part |
math.is_nan(x) |
true if x is NaN |
math.is_inf(x) |
true if x is infinity |
math.PI |
π |
math.E |
e |
math.INF |
positive infinity |
require <stdr/io> — input and file I/O
| Function | Description |
|---|---|
io.println(s) |
print string + newline to stdout |
io.print(s) |
print string to stdout |
io.eprint(s) |
print string to stderr |
io.eprintln(s) |
print string + newline to stderr |
io.flush() |
flush stdout |
io.read_line() |
read a line from stdin |
io.read_int() |
read an integer from stdin |
io.read_float() |
read a float from stdin |
io.read_char() |
read a single character from stdin |
io.open(path, mode) |
open a file, returns file handle |
io.close(f) |
close a file handle |
io.read_line_file(f) |
read a line from an open file |
io.read_all(f) |
read entire file contents |
io.write(f, s) |
write string to file |
io.writeln(f, s) |
write string + newline to file |
io.seek(f, off, whence) |
seek within file |
io.tell(f) |
current file position |
io.eof(f) |
true if at end of file |
io.flush_file(f) |
flush an open file handle |
io.exists(path) |
true if file exists |
io.size(path) |
file size in bytes |
require <stdr/os> — operating system
| Function | Description |
|---|---|
os.time() |
current Unix timestamp |
os.clock() |
CPU time used by process |
os.time_str(t) |
format timestamp as YYYY-MM-DD HH:MM:SS |
os.time_parts(t) |
table with year, month, day, hour, minute, second |
os.getenv(name) |
get environment variable |
os.setenv(name, val) |
set environment variable |
os.exit(code) |
exit with status code |
os.args() |
returns script arguments as a 1-based tif array |
os.cwd() |
current working directory |
os.mkdir(path) |
create directory (with parents) |
os.remove(path) |
delete file or empty directory |
os.rename(from, to) |
rename or move a file |
os.platform() |
"linux", "macos" or "windows" |
Features
- Variable declaration with
let, typed or untyped - Arithmetic, comparison and logical operators
if,else,else ifwhileandforloops- Functions with type checking
- Lambda expressions and closures (first-class functions)
- Higher-order functions (functions as values)
- Arrays with enforced element types
- Structs for structured data
- Pattern matching (
match) with classiccaseand ccl-style=>forms - String interpolation
- Module system and imports
- Pointers and manual memory interaction via THD
- Scoped environments
- Stack overflow protection
- Basic UTF-8 tolerance in source scanning
- Ahead-of-time compilation to native binaries (-o flag)
- Lusotif syntax support (--luso flag)
Examples
Structs:
require<std>struct Point{float:: xfloat:: y}letp=Point{x=1.0,y=2.0}std.println(p.x)Pointers:
require<std>fn double(*int:: n)-> void{*n=*n*2}letint:: x=5double(&x)std.println(x)Arrays:
require<std>let<int>nums[3]=[1,2,3]forninnums{std.println(n)}Countdown loop:
letx=5whilex>0{print(x)x=x-1}Typed function with return type checking:
require<std>fn sum(int:: a,int:: b)-> int{returna+b}letint::x=10x=sum(x,5)std.println(x)Boolean logic:
require<std>leta=trueletb=falseifaandnotb{std.println("ok")}Recursive function:
require<std>fn int:: factorial(int:: n){ifn==0{return1}returnn*factorial(n-1)}std.println(factorial(6))Lambda expressions and closures:
require<std>letdouble=fn (x){returnx*2}std.println(double(5))// 10
letmake_multiplier=fn :: int(int:: factor){returnfn :: int(int:: x){returnx*factor}}lettimes3=make_multiplier(3)std.println(times3(7))// 21
Optional parameters and typed interpolation together:
require<std>fn describe(?int:: start,int:: stop)-> string{return"range from #(int){start} to #(int){stop}"}std.println(describe(5,9))std.println(describe(9))Binary Compilation
tif supports two binary-style output modes:
-obuilds a standalone native ELF binary for the current platform-ppackages the source into a portable.ptftif binary that can be shared across platforms and run by compatibletifandlusotifruntimes
Both modes now bundle imported .tif modules alongside the main program, so local and -I-resolved source modules can run without shipping those extra source files separately.
The current -o path no longer emits a Lua launcher: it generates GNU assembler for Linux x86-64 and links it into a self-contained executable with no Lua runtime dependency.
tif -o myprogram myprogram.tif
./myprogram
Portable package example:
tif -p myprogram myprogram.tif
tif myprogram.ptf
lusotif myprogram.ptf
The current native backend targets Linux x86-64 and focuses on a practical subset of the language:
require <std>let, assignment,if,while- top-level functions, recursion, and optional typed parameters
print,std.print(...),std.println(...)- integer arithmetic and boolean comparisons
- string literals in print calls
When the program imports extra .tif source modules, tif -o now falls back to a native launcher build that embeds the main source plus the imported module graph.
Features such as arrays, pointers, structs, modules beyond <std>, lambdas, matches, and runtime-loaded .so modules are not yet supported by the native backend. In those cases, tif -o fails with a compile-time diagnostic instead of silently producing a launcher binary.
Example native-friendly program:
require<std>fn factorial(int:: n)-> int{ifn==0{return1}returnn*factorial(n-1)}std.println(factorial(6))There is also a ready-to-compile example in examples/native_aot.tif.
For Lusotif syntax, use:
tif --luso -o myprogram myprogram.tif
Lusotif Support
Lusotif is a variant of tif with Portuguese keywords, error messages, and standard library. The --luso flag accepts Lusotif syntax and translates it to tif internally before execution or compilation. Use --luso-dump to output the translated tif code to stdout.
Installation
tif is distributed as a native binary. By default, the build process targets LuaJIT, compiles the Lua sources to LuaJIT bytecode with luajit -b, and links them into a standalone C executable through the Lua C API. Lua 5.4 is still supported as an alternative compatibility/runtime path: in that mode tif compiles the Lua sources with luac and installs the corresponding bytecode files, because LuaJIT and Lua 5.4 bytecode are not compatible with each other.
Interactive installer
The recommended way to install, update, or uninstall tif is through the interactive menu:
git clone https://codeberg.org/grassleaff/tif.git
cd tif
sudo bash tifmenu.sh
The installer first lets you choose the embedded runtime:
- LuaJIT — default and recommended optimized runtime
- Lua 5.4 — compatibility/runtime alternative
It then presents a navigable menu (arrow keys + Enter) with three options:
- install — fetches available versions from Codeberg, lets you pick one, and builds and installs it
- uninstall — removes the binary and all bytecode files from the system
- update / downgrade — lists all available versions so you can switch to any of them
Dependencies for Lua 5.4
| Dependency | Purpose |
|---|---|
lua5.4 |
Runtime (luac + interpreter) |
liblua5.4-dev |
C headers for linking |
gcc |
Compiling the C launcher |
pkg-config |
Resolving Lua compile/link flags |
git |
Cloning the selected version |
On Debian/Ubuntu:
sudo apt install lua5.4 liblua5.4-dev gcc pkg-config git
On Arch Linux:
sudo pacman -S lua gcc pkgconf git
Dependencies for LuaJIT
| Dependency | Purpose |
|---|---|
luajit |
Runtime |
libluajit-5.1-dev |
C headers for linking |
gcc |
Compiling the C launcher |
git |
Cloning the selected version |
On Debian/Ubuntu:
sudo apt install luajit libluajit-5.1-dev gcc git
Usage
tif file.tif
tif file.tif hello world
tif -o myprogram file.tif
./myprogram
tif --help
tif --version
Lusotif translation mode (accepts Portuguese-syntax variant):
tif --luso arquivo.tif
tif --luso-dump arquivo.tif > traduzido.tif
Running from source
git clone https://codeberg.org/grassleaff/tif.git
cd tif
make all
luajit src/main.lua file.tif
Or, with Lua 5.4:
make all RUNTIME=lua
lua5.4 src/main.lua file.tif
Writing C libraries for tif
Any C shared library that exposes a luaopen_<name> function can be loaded by tif as a module. The interpreter wraps the returned Lua table into a tif namespace automatically — no changes to the interpreter are needed for third-party libraries.
Minimal example
// mylib.c
#include <lua5.4/lua.h>#include <lua5.4/lualib.h>#include <lua5.4/lauxlib.h>
static int l_greet(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_pushfstring(L, "hello, %s!", name);
return 1;
}
static const luaL_Reg mylib[] = {
{ "greet", l_greet },
{ NULL, NULL }
};
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
gcc -shared -fPIC -O2 -o mylib.so mylib.c $(pkg-config --cflags --libs lua5.4)
// place mylib.so next to your .tif file, or in a -I path
require"mylib"// local import (mylib.so next to source file)
// or:
tif-Ilibs/main.tif// global import from libs/
require<std>require"mylib"std.println(mylib.greet("tif"))// hello, tif!
Rules for tif-compatible C libraries
- The entry point must be named
luaopen_<stem>where<stem>matches the.sofilename without the extension. Formylib.sothe function isluaopen_mylib. - Return a single Lua table from
luaopen_*. All entries in the table become callable members of the tif namespace. - Functions receive arguments as Lua values on the stack. Use
luaL_checkstring,luaL_checknumber,luaL_checkintegeretc. to validate them. - Return values are pushed onto the Lua stack. Returning
1means one return value. - tif arrays are Lua tables with
__is_array = trueand an integer fieldnholding the element count. Elements are 1-based. - tif pointers are Lua tables with
__is_ptr = true, ahandleinteger, andbase_typestring. - Errors should be raised with
luaL_error(L, "message")— the interpreter catches them and displays them with the source line.
Distributing a library
A third-party library for tif is just a .so file. Users install it by:
- Placing it anywhere on the filesystem
- Passing the directory with
-Iwhen running tif:
tif -I./libs main.tif
Or by dropping it into /usr/local/lib/tif/ for system-wide availability without -I.
License
MIT License — Copyright (c) 2026 grassleaff. All Rights Reserved.