A lightweight, embeddable, data-centric scripting language whose reach extends from JSON/FD data all the way down to runtime-compiled C and raw machine code.
English (this file) · 简体中文
- Version: 9.5
- Website: https://funlang.org
- Author: Zhang Weidong <zwd@funlang.org>
- License: MIT, plus commercial terms (see License)
Version 9.5: this is not a toy or a prototype — a mature language refined and production-validated for over 15 years (2010–present), with a lineage that reaches back through Nuva (2006) and TemplateScript (2005).
- Why Fun
- Core features
- Quick start
- Packages
- Testing
- A taste of Fun
- The capability ladder
- The FD data format
- Foreign function interface
- Keyword aliasing
- Embedding with fun.dll
- Repository layout
- Building
- Standard library
- Examples
- License
In one sentence: Fun is a scripting language that goes all the way from JSON to machine code.
- One runtime spans the whole spectrum, from high-level data work (JSON/FD) down to low-level system programming (FFI, C at runtime, JIT, machine code);
- 76 standard-library modules written in Fun itself ship together as one embeddable
fun.dll; - in AI-agent and domestic-IT scenarios, it is the lightweight glue connecting OSes, databases, chips, and business logic.
Fun is designed around a simple premise: data and code should be equally first-class, and a script should be able to reach down to the machine without leaving the language.
Most scripting languages either stay high-level (easy, but you hit a wall at the FFI) or force you to write C for performance-critical parts. Fun collapses that ladder into one runtime:
- Manipulate JSON and FD data with native list / set / tree collections;
- call arbitrary native code through a compact FFI signature;
- compile C at runtime with a bundled Tiny C Compiler;
- drop to inline assembly / JIT for the tightest loops;
all from pure script, in a single embeddable binary.
Fun is written in Pascal (Delphi / Free Pascal). The core is a small tree-walking interpreter with a dynamic value model, an object system, closures, and modules — deliberately compact so it can be embedded as fun.dll or run as a standalone fun.exe.
- Dynamic, data-centric value model — numbers, strings, time, regex, and first-class collections: list (array), set, and tree, built on a variant-based runtime.
- First-class functions — anonymous functions, function references, currying, default and variadic parameters, and
->pipelines. - Object system —
class,this/base, methods, and property get/set dispatch (methods, getters and setters are dispatched uniformly through the.operator). - Modules —
use 'file.fun'(optionallyas alias) pulls in other scripts; the runtime tracks module state and dependencies. - Regex as a value type — bundled PCRE engine with inline regex literals (
/.../) and match operators (=~,!~). - Built-in data formats — JSON and FD, a JSON-compatible, Markdown-like format friendly to humans and to AI (see below).
- Runtime C & machine code — bundled Tiny C Compiler, JIT, and inline assembly (see the capability ladder).
- Compact FFI — call DLL/shared-library functions with a terse signature string (see FFI).
- Keyword aliasing — a canonical English keyword set with pluggable alias packs (Chinese, French, custom) selected at runtime (see below).
- Cross-platform — Windows (32/64-bit), Linux, ARM, and Windows CE.
- Dual distribution — command-line
fun.exeand embeddable runtimefun.dllexporting a simpleRuninterface. - Self-hosted standard library — 76 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.
Run a script:
fun.exe <file.fun> # run a script (.fun / .foo / .fxx)
fun.exe -v <file.fun> # show compile & run timing
fun.exe -log[:log.txt] -gui # log output / run in GUI mode
fun.exe -key:key_cn.ini # load a keyword alias pack
Hello world (canonical keywords):
fun foo()
?. 'Hello, fun!';
end fun;
foo();
fun max(a, b)
if a > b then
result = a;
else
result = b;
end if;
end fun;
?. max(1, 9);
?. max(9, 1);Run:
> fun.exe fun/demo/fun.fun
Hello, fun!
9
9
fun below is the command-line binary (fun.exe on Windows). A project is a
directory with a package.fun manifest; fun package init creates the
manifest and a minimal template:
fun package init myapp # package.fun + src/main.fun
fun package init mylib --lib # package.fun + src/lib.fun
fun package()
result = [
name: 'myapp',
version: '0.1.0',
bin: 'src/main.fun',
lib: '',
'fun': '9.4',
dependencies: [
'hello': [source: 'github.com/acme/hello', version: 'v1.0.0'],
'local': [source: '../libs/local']
],
// >>> generated - do not edit; run 'fun package tidy' to refresh
resolved: []
// <<< generated
];
end fun;fun package add <name> <source> [--version <v>]andremove <name>edit only thedependenciesset; versions are exact tags, branches, or commits.fun package tidyinstalls the declared revisions into the shared~/.funcache (FUN_HOMEoverrides it), verifies git tree hashes, and writes theresolvedblock. Path dependencies are used in place; only git dependencies needgit.fun run [args...]runs the manifestbinfrom the project root. Inside a project,fun <script>is the flat mode of the same launcher: it runs the given script with the project's dependencies available.fun packembeds the app and its dependency libraries into one executable; the result runs without~/.fun.
Runs are read-only: a missing or stale dependency reports
run 'fun package tidy' instead of fetching anything. FUN_PACKAGE_READY=1
skips project mode (useful for a broken manifest). See
fun/demos/package-demo for a runnable example.
Test files are named *_test.fun; their location is free (tests/ is the
recommended layout). Other .fun files are ordinary modules, so tests can
share helpers. A test file imports test.fun and registers blocks:
use 'test.fun';
use '../src/lib.fun';
test('adds', {
assert.equal(add(1, 2), 3);
});fun test
fun test runs every *_test.fun below the project root, one child process
per file, prints PASS/FAIL per file, and exits non-zero when any test
fails. A project only needs package.fun (a bin entry is not required);
manifest dependencies are available through the same launcher as fun run.
- Assertions:
assert.equal,not_equal,is_true,is_false,match,contains,greater,less,is_nil,not_nil,raises,fail; each takes an optional trailing message. test.fun, the globalstest/assert/runTests, the aliasesas test/as assert, and dependency keystest/assertare reserved. A third-party test library can be imported under another alias.dist/, dot-directories, and path-dependency directories are skipped.
See fun/demos/test-demo for a runnable example.
Two quick, self-contained snippets that show both ends of the language (the full files live in fun/demos/benchmarks/).
Custom operators on classes, first-class functions, composition, and pipelines:
# a class with a custom operator
class Number(me)
this['#'] = you -> (me + you) * me * you;
end class;
var $ = Number(2);
?. $ .# 3; # (2 + 3) * 2 * 3 = 30
# first-class functions, composition & pipelines
var f = a -> a * 2;
var g = a -> a + 3;
?. g(f(1 + 2) + 3); # 12
?. 1 + 2 | f + 3 | g; # 12 (same, pipelined)A data pipeline that turns a query string into a map using regex and collection combinators:
use 'lib-regex.fun';
use 'lib-set.fun';
var q = 'a=1&b=2&c=';
var parseQuery = s -> (s | split(/&/) | map(sp_eq) | fromPairs);
?. parseQuery(q).@toJson(1); # {"a":"1","b":"2","c":""}See test-high-level-language.fun for the complete version.
What makes Fun unusual is how far a script can go without a separate toolchain:
| Layer | Facility | Backing |
|---|---|---|
| High-level | JSON / FD data, list / set / tree | native collections and parsers |
| Native calls | dll.getapi(name, 'signature') |
FFI with a compact signature convention |
| C at runtime | ccompile(code, ...) |
bundled Tiny C Compiler (libtcc.dll/.so) |
| JIT | NewJit(...) |
dispatch between C (TCC) and assembly |
| Machine code | Assembly(code, ...).Load() |
inline assembly, executable memory allocation |
For example, lib-tcc loads libtcc.dll on Windows (and libtcc.so on Linux) and exposes tcc_new, tcc_compile_string, tcc_get_symbol, ... so you can compile C source from a string and call the resulting symbol immediately. lib-asm allocates executable memory, writes machine code, and invokes it; lib-jit picks the right path based on the source (#!c → TCC, otherwise assembly).
A single script can mix both: NewJit compiles C and inline assembly at runtime, and each can call straight back into a Fun function (@toCallback). The snippet below sums 1..n — once via assembly, once via compiled C — and hands the result to a Fun callback both times (full version: test-jit-cb.fun):
use 'lib-jit.fun';
fun cb(a, b) # a Fun callback called from JIT'd code
result = a * 2^32 + b;
end fun;
# assembly path
var asm = `#!asm i:i
mov ecx, dword ptr [esp+04]
@sum1ton
push eax
push edx
mov eax, <test>
call eax
`;
var jit = NewJit(asm, names: [test: cb.@toCallback(nil, 'ii:i', true)]);
?. jit.Run(100000);
# C path (compiled at runtime with bundled TCC)
var c = `#!C ii:i
int sum(int n, int (*test)(int, int)) {
long long s = 0;
for (int i = 1; i <= n; i++) s += i;
test((int)(s >> 32), (int)(s & 0xFFFFFFFF));
return 1;
}
`;
jit = NewJit(c);
?. jit.call(100000, cb.@toCallback(nil, 'ii:C', true));Platform note: the
#!asmblock above is 32-bit x86 (Windows) assembly (esp-relative args,ecx). The#!Cpath is portable, but the assembly snippet is architecture-specific — on Linux/ARM use the C form or rewrite the assembly for the target ABI.
FD is a data format designed to be readable by humans and by AI, and interoperable with JSON:
- JSON-compatible structure (nested keys, arrays);
- indentation-based, Markdown-like syntax;
- optional SSE compression;
- bidirectional conversion with JSON (
@toJson,getJson).
# FD format (Markdown-like: indentation + space-separated keys)
person
name Zhang Weidong
age 46
skills
- pascal
- c
- fun
# equivalent JSON
{"person": {"name": "Zhang Weidong", "age": 46, "skills": ["pascal", "c", "fun"]}}
Its defining trait is that FD is self-describing: the parser in lib-fd.fun is generated from a BNF grammar written in FD itself (fd.bnf.fd). The grammar file src/parse/bnf/fun.ebnf documents the language grammar in the same spirit.
FD value rules (shared by the interpreter, fd.js/fd2.js, and the BNF
reader):
- Numbers use the RFC 8259 grammar, the same as
getJson(json: 2):-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?. Leading zeros (03), hex,.5,1.,+1and1eare strings. Values follow the host number type: Fun/Pascal use Int64, JS uses IEEE-754 doubles, so integers beyond 2^53 lose exactness in JS, and literals beyond the host type stay strings (beyond Int64 in Fun, beyond a double in JS). Quote large IDs as strings when they must round-trip exactly. Writers use the same human-friendly 15 significant digits astoStr, so0.1 + 0.2is written as0.3. Tiny floating-point round-trip differences are accepted for readability. - Only
true,falseandnullare literals.yes/noare strings here; Fun's source-level string truthiness accepts them, but the data formats do not. - Strings are raw by default. Prefixing the opening quote with a backslash
enables escapes (
\"a\nb",\xHH); an unknown escape is preserved literally. #!kvd:<char>selects a custom key/value delimiter for the rest of the document (for example#!kvd:=andkey=value).- Duplicate named keys are last-wins, matching
JSON.parse.
Fun exposes native functions through a compact signature-string convention. Examples drawn from the standard library:
'kernel32'.getapi('VirtualAlloc', 'iiii:i'); // 4 int args -> int
'tcc'.getapi('tcc_new', 'v:i'); // void arg -> intThe signature encodes each argument's type, then :, then the return type (i = int, s = string, v = void, d = double, etc.). The same mechanism drives COM (lib-winole), the Windows API (lib-winapi), and the C runtime.
getapi is per-platform: winapi.pas on 32-bit Windows, and lffi.pas - backed by FPC's bundled libffi - on Linux, macOS and win64 (there it resolves against ffi.dll).
The interpreter's canonical keywords are English (fun, if, then, loop, class, ...). Because keywords are resolved through an alias layer (ParseAlias), you can supply pluggable alias packs at startup:
fun.exe -key:key_cn.ini # Chinese keywords (函数=fun, 如果=if, ...)
fun.exe -key:key_fr.ini # French keywords
Included packs: key_cn.ini (简体中文), key_fr.ini (français), and key_pua.ini (custom/private-use). The language itself is keyword-neutral — aliases are a localization/accessibility feature, not a different language.
fun.dll is the embeddable runtime for integrating Fun scripts into third-party applications:
Run(PChar('script.fun')); // compile the script
Run(scriptContent); // execute it
Run(-1); // release- Free use (MIT): personal study, internal tools, and products released under an open-source license.
- Commercial use: embedding
fun.dllin a closed-source commercial product (exe/dmg/apk or a SaaS backend) requires a commercial license — seeLICENSE.dll.
fun/
├── src/
│ ├── core/ # Interpreter core (value model, object system, control flow, IO)
│ ├── parse/ # Lexer/parser
│ │ └── bnf/ # Grammar: fun.ebnf, yacc.y, lex.l (generates .inc/.cod)
│ ├── lib/ # Built-in runtime library (winapi, winole, UI, host, etc.)
│ ├── regex/pcre/ # Bundled PCRE regex engine
│ ├── 3rd/ # Third-party components (KOL)
│ ├── utils/ # Utility functions
│ └── prj/fun/ # Projects & build scripts (funcmd.dpr, Delphi/FPC)
├── fun/
│ ├── lib/ # Standard library (pure Fun, 76 modules)
│ ├── demo/ # Language syntax demos
│ ├── demos/ # Full applications & benchmarks
│ └── key_*.ini # Keyword alias packs (CN / FR / PUA)
└── LICENSE* # License files
The Fun core is Pascal; the source entry point is src/prj/fun/funcmd.dpr.
Toolchain locations for the Windows-hosted .bat scripts are centralized in one file: src/prj/fun/setenv.bat. Edit it once to point at your installed Delphi / Free Pascal, or set the FPC, DELPHI2006, DELPHI2009 environment variables beforehand — every make-*.bat script calls it and picks up the paths.
Each script is <toolchain>-<target>; the build machine is where it runs, which differs from the target for the cross rows:
| Target | Build machine | Script | Toolchain |
|---|---|---|---|
| Windows 32-bit | Windows | src/prj/fun/make-2006.bat / -2009.bat |
Delphi 2006 / 2009 |
| Windows 64-bit | Windows | make-win64.bat |
FPC cross |
| Windows CE / ARM | Windows | make-wince.bat |
FPC cross |
| Linux (i386) | Windows | make-linux.bat |
FPC 2.4.0 cross |
| Linux / ARM | Windows | make-arm-linux.bat |
FPC cross |
| Linux x86_64 (native) | Linux x86_64 | src/prj/fun/make-linux-x86_64.sh |
FPC + gcc |
| Linux aarch64 (arm64) | Linux x86_64 | src/prj/fun/make-linux-aarch64.sh |
FPC cross + Zig |
| macOS x86_64 / aarch64 | Linux x86_64 | src/prj/fun/make-darwin.sh |
FPC cross + Zig |
| macOS (native) | macOS | src/prj/fun/make-macos.sh |
FPC (Homebrew) |
Building with -DFunDll produces the embeddable runtime fun.dll, which exports the Run interface. Build artifacts are covered by .gitignore and are not versioned.
The full grammar is defined in src/parse/bnf/fun.ebnf.
The last two rows are the only builds that need a bootstrapped toolchain. Both run on a Linux x86_64 build machine and produce a native target binary with no target SDK and no target binutils — the Mach-O and aarch64 assembler/linker come from the clang/lld bundled with Zig 0.13:
cd src/prj/fun
./bootstrap-darwin-cross.sh # once: FPC darwin cross toolchain + Zig (~/.fun/darwin-cross, ~2 GB)
CPU=all ./make-darwin.sh # -> fun/funcmd-darwin-{x86_64,aarch64}
./bootstrap-linux-aarch64.sh # once: aarch64-linux RTL, reusing ppcrossa64 + Zig from the darwin tree
./make-linux-aarch64.sh # -> fun/funcmd-linux-aarch64- Build machine requirements: Linux x86_64, host FPC 3.2.x,
make,curlorwget,tar, and ~2 GB free disk. Zig is fetched by the bootstrap (pinned to 0.13; 0.14 panics on older glibc). No macOS host, Apple SDK, orosxcrossis required — Zig ships a libSystem stub. bootstrap-linux-aarch64.shreuses theppcrossa64and Zig from the darwin toolchain: an FPC compiler binary is CPU-specific but can target any OS it has units for.- Optional features degrade as usual: no PCRE -> no regex, no libffi unit/stub -> no
getapi(the win64 CI supplies both,pcre3.dllandffi.dll). The Linux aarch64 artifact is the core interpreter (its-dRegexand-dLinuxFFIneed a libc / libffi that are not cross-built yet). - Porting details:
art/notes/macos-port.md.
To keep the repository small and license-clean, the following third-party sources/binaries are not shipped. If one is missing the build script degrades and prints a notice instead of failing:
| Component | Where to put it | Where to get it | Who builds it |
|---|---|---|---|
| PCRE 8 (8-bit) | src/3rd/pcre-8/ |
https://www.pcre.org/ (8.x source, e.g. 8.45) | Linux: make-linux-x86_64.sh compiles it with gcc (static, -dRegex); Windows (i386): src/regex/pcre/make.bat (Borland bcc32 + make, -Dpcresrc=<path>; the objects pcrd.pas {$LINK}s); win64: the CI builds pcre3.dll with MinGW-w64 gcc and links it dynamically (-dRegex -dPCRE_WinDLL, see below) |
| Tiny C Compiler 0.9.27 | src/3rd/tcc/ |
the tinycc release on savannah/nongnu, or repo.or.cz tag release_0_9_27 |
make-linux-x86_64.sh produces libtcc.so (placed next to funcmd) |
| libffi (win64) | ffi.dll next to funcmd.exe |
MSYS2 mingw-w64-x86_64-libffi, or any x86_64 libffi build |
the win64 CI installs it so getapi works there (-dWinFFI); Linux/macOS use the libffi that ships with FPC |
| Windows runtime DLLs | fun/libtcc.dll, fun/zlib.dll |
official release or build them yourself | no build step; keep them beside fun.exe for ccompile / NewJit |
- Once the directories are in place,
cd src/prj/fun && ./make-linux-x86_64.shbuilds PCRE and TCC automatically. - Or point at them explicitly:
PCRE_SRC=/path/to/pcre TCC_SRC=/path/to/tcc ./make-linux-x86_64.sh. - No PCRE -> the build has no regex (
-dRegexoff); no TCC -> nolibtcc.so, solib-tccis unavailable. Neither affects the core interpreter. - Convenience fetcher:
src/prj/fun/fetch-3rd-party.shdownloads and unpacks the pinned PCRE 8.45 and TCC 0.9.27 releases intosrc/3rd/(used by CI).
After building, run the snapshot suite from the repository root:
fun/test/run-linux-regression.sh # Linux (or any POSIX host: auto-detects)
fun/test/run-macos-regression.sh # macOS wrapper; skips the Linux-only tests
fun/test/run-windows-regression.bat # Windows counterpartThe runner is host-aware: fun/test/regression-skip-<platform>.txt lists tests
that cannot run on a host (a /proc or libc.so.6 backend, an unported JIT,
...) and fun/test/expected/<platform>/ can hold host-specific snapshots that
override the shared fun/test/expected/. --update writes to the host's own
snapshot directory; macOS details and the current skip reasons are in
art/notes/macos-port.md.
.github/workflows/regression.yml runs the
suite on five platforms - Linux, macOS and Windows on x64, plus Linux and macOS
on arm64. All five build with Free Pascal and are a gate. The win64 build gets
PCRE from pcre3.dll, which the job compiles with the runner's MinGW-w64 gcc
(the bcc32 objects belong to the 32-bit build and MinGW objects cannot be linked
into funcmd, so pcrd.pas imports the DLL under -dPCRE_WinDLL);
run-windows-regression.bat still probes for the engine at startup and reports
the regex-dependent tests as SKIP when a build has none, so a regex-less
toolchain degrades instead of failing.
The same job installs libffi from the runner's MSYS2 and builds win64 with
-dWinFFI when the resulting ffi.dll sits next to funcmd.exe, which is what
turns on getapi there: FPC's bundled ffi unit already has a WIN64 target, so
the only thing missing on Windows was the library. Both optional features follow
the same rule - if the DLL or the unit is absent the job falls back to the core
interpreter instead of failing, and a probe step reports what the build ended up
with.
With getapi in place, the Windows job also builds the FFI test helper
(fun/test/ffi/libffitest.c) into libffitest.dll with the same MinGW gcc and
runs the four host-independent FFI cases (ffi-int64, ffi-float,
ffi-float-cb, ffi-callbacks) against the shared snapshots, so 64-bit
integers, float/mixed arguments and @toCallback closures are covered on win64
too; ffi-scalar stays Unix-only (it calls getpid/libm directly).
DyLibWin() in lib-host.fun picks the helper's name per host, and the runner
probes for both the DLL and a working getapi, adding those cases to its skip
list when either is missing.
windows-arm64 is disabled: FPC 3.2.2, the pinned toolchain, has no
win64/aarch64 target, so there is nothing to build with.
For untrusted scripts, build the separate capability-restricted interpreter:
cd src/prj/fun && SAFE=strict ./make-linux-x86_64.sh # produces funcmd-safe
fun/test/run-safe-regression.sh strict # focused capability testsfuncmd-safe (-dFunSafe) removes native code (getapi / @toCallback /
COM), arbitrary-address memory access, file access, and compile-from-file or
inline code loading at compile time. Pick the level in the build script:
SAFE=strict blocks all of them, SAFE=readonly allows file reads,
SAFE=readwrite also allows writes, and SAFE=custom SAFE_ALLOW=read,write,compile selects categories explicitly. The default
funcmd and its full regression suite are unchanged; a separate linux-safe
CI job verifies strict and readonly, and it only runs on manual dispatch
(safe_only), not on push/PR.
The standard library in fun/lib is written in pure Fun (76 modules):
In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 76 modules written in Fun itself.
- Data:
lib-json,csv,toml,jsonschema,lib-yaml,lib-xml,lib-base64,lib-cstruct,lib-md5,lib-crypt - Utility:
uuid - Collections / algorithms:
lib-set,lib-tree,lib-stack,lib-dyns,lib-math - Text:
lib-string,lib-regex,lib-match,lib-unicode - System / IO:
lib-file,lib-os,lib-host,lib-path,env,cli,fs,io,logging,lib-time,lib-cmdline,lib-proc - Network:
lib-winsock,lib-ajax,lib-jsonrpc,http-server,http-client,http-auth,http-bearer,http-cors,http-session - Databases:
lib-orm,lib-orm-pro,lib-orm-gen,lib-orm-rpc,lib-orm-cte,lib-ado,lib-ado-schema - Native / low-level:
lib-winapi,lib-winole,lib-tcc(C compile),lib-jit,lib-asm,lib-asm-pro - UI:
lib-ui,lib-ui-base,lib-dialog,lib-trayicon - Concurrency / async:
lib-async,lib-jsasync,lib-bind,lib-message - Testing:
test(registration and assertions forfun test)
Logging is configured once and then used through named loggers:
use 'logging.fun';
logging.config([level: 'debug']);
var logger = logging.getLogger('http');
logger.info('request', [path: '/users']);The default is stderr + info + text. logging.config can replace handlers and
formatters; logging.newLogger creates an independent logger for tests or a
dedicated sink.
Environment variables are available without platform-specific FFI:
use 'env.fun';
var home = env.get('HOME');
var port = env.get('PORT', '8080');cli is a small command framework. A root command owns global flags and
subcommands, and every subcommand is the same cli.command object. Help,
version, aliases, env fallback, required flags, hooks and exit codes are built
in.
use 'cli.fun';
fun hello(ctx)
var times = ctx.int('times');
for i = 1 to times do
?. 'hello ' & ctx.string('name');
end do;
end fun;
fun serve(ctx)
?. 'serving on port ' & ctx.int('port');
end fun;
var root = cli.command('demo', [
usage: 'demo command-line tool',
version: '1.0.0'
]);
root.flag(cli.string('name', [aliases: ['n'], value: 'world', usage: 'who to greet']));
root.flag(cli.int('times', [aliases: ['t'], value: 1, usage: 'repeat count']));
root.action(hello);
root.command(cli.command('serve', [
usage: 'start the server',
flags: [cli.int('port', [aliases: ['p'], value: 8080, usage: 'listen port'])],
action: serve
]));
var code = root.run(); // nil: process arguments
if code <> 0 then cli.quit(code); end if;$ demo --name bob -t 2
hello bob
hello bob
$ demo serve -p 9000
serving on port 9000
$ demo --help
$ demo --version
cli.command(name, opts) accepts usage (a one-line summary in the parent's
command list, also shown in its own help when description is empty),
usageText (replace the generated usage line), argsUsage, version,
description, aliases, flags, commands, action, before, after,
hidden, out and err.
Flags come from cli.string, cli.bool, cli.int, cli.float and
cli.strings; options are aliases, value (default), usage, env
(a name or list of names), required, hidden and placeholder. strings
may be repeated. Its default list is used only when the flag is absent; the
first explicit value replaces the default. Help shows default values and env
names. A required flag must appear on the command line or come from env; a
default does not satisfy it, while an empty --flag= still counts as supplied
(validate non-empty values in the action). Flag and command names are
case-sensitive, so -V and -v are different flags. Hidden flags and commands
still parse but are omitted from help. Duplicate flag/command names and
aliases raise at construction.
Short and long forms may be mixed: --name=value, --name value, -n value,
-n=value, combined bools such as -ab when both are bool flags, and -- to
stop option parsing. Boolean values accept true/false, 1/0, yes/no and
on/off; bool flags do not consume the next token, so use --flag=false.
Integers are decimal; floats accept decimals and exponents. A non-bool option
takes the next token as its value; when that value starts with
-, use the attached form --name=-x/-n=-x (negative int/float values such
as --times -1 are still accepted). Global flags belong before the subcommand;
tokens after it are parsed by the subcommand. When a command has subcommands,
its first positional token is treated as a command name, so use -- before
positional arguments of the root action.
The action receives a context: ctx.string/bool/int/float/strings(name),
ctx.has(name) (true only when set on the command line or by env),
ctx.args(), ctx.arg(n), ctx.narg(), ctx.command(), and the configured
writers ctx.out()/ctx.err(). Typed accessors validate the declared flag:
unknown names or a wrong type raise instead of silently returning a zero value.
Parent flags inherited by a child stay accessible. Use canonical flag names,
not aliases, with these accessors. Env values are converted by flag type;
strings values are split on commas. The first present name from env wins.
Help and version are automatic: --help/-h, help [command...], and, on a
command with version set, -v/--version. The help command exists at the
root and on commands with subcommands; a leaf command can still use help as a
positional value. help/h are reserved flag names, and version/v are
reserved on a command that has a version; registration raises if a flag tries
to use them. Subcommand help lists its own options; global options are listed by
the parent/root help.
Hooks run without process-level setup: before(ctx) may return nil/true/0 to
continue, false to stop with code 1, or a nonzero integer to stop with that
code. A parent before runs before the child action, and a parent after runs
after the child action, including when the action raises. Help, version and
parse errors do not run parent hooks. after(ctx) runs after its action returns
or raises, once before has succeeded; its return value is ignored.
run(args) never exits the process; it returns an exit code. Pass a list for
exact control, or call run() to read process arguments (a leading
.fun/.foo/.fxx script path is dropped; an empty process argument ends the
scan). Use cli.quit(code) after the command has finished when the process
should exit. Mixing ?. with redirected out/err can reorder output: ?.
always writes to the process console, while ctx.out()/ctx.err() use the
selected writers.
Exit codes: parse errors, unknown flags/commands, bad values and missing
required flags return 2; a false hook or an exception returns 1; an integer
returned by an action becomes the exit code. Actions must return nil, a boolean
or an integer; other return types raise and return 1. before accepts nil, a
boolean or an integer.
Help goes to stdout and errors go to stderr by default. The default console
writer uses the process handles directly; pass out/err for captured,
redirected or non-ASCII output. Any object with write(s) works; actions reach
them through ctx.out() and ctx.err(), and a child's own writers override the
root's. Writers are not closed or flushed by the framework. Tests use
io.buffer:
use 'io.fun';
var out = io.buffer();
var app = cli.command('demo', [out: out, err: out]);
app.flag(cli.string('name', [value: 'world']));
app.flag(cli.int('times', [value: 1]));
app.action(hello);
?. app.run(['--help']) = 0;
?. out.text().subpos('usage: demo') >= 0;Filesystem helpers cover directories, walking and temporary entries:
use 'fs.fun';
fs.mkdirAll('build/out');
fs.walk('src', (p, isDir) { ... });
fs.atomicWrite('build/out/config.json', data);walk and removeAll do not follow symbolic links (removeAll deletes the
link itself, including broken links); exists/isDir/isFile follow them.
Whole-file create/read/append stays on the built-ins — save(data),
save(data, append: true) and load().
For segmented or binary I/O, fs.open returns a byte-oriented file handle:
var f = fs.open('data.bin', 'r+');
f.seek(16, 0);
var head = f.read(4);
f.write(bytes);
f.close();Modes are r, w, a, r+, w+, a+ (an optional b is accepted);
mode defaults to r, and a/a+ append writes. Methods are read(n),
write(data), seek(offset, whence), tell(), eof(), flush() and
close(); whence defaults to 0 (start) and may be 1 (current) or 2 (end).
read returns a byte string
(characters 0..255) and write writes each character's low byte. Open/I/O
failures raise; read at EOF returns ''. Call close() explicitly.
Read/write switching follows C stdio: seek before writing after reading;
seek or flush before reading after writing. Offsets are 64-bit on 64-bit
hosts; large literals need arithmetic construction (e.g. 0xFFFFFFFF + 1).
On Windows fs.open requires a 64-bit build; 64-bit narrow-character builds
use the active ANSI code page for paths.
Generic stream helpers accept any reader/writer, do not close the handle, and
treat '' as EOF:
use 'io.fun';
var b = io.buffer('hello');
var text = io.readAll(b); // small data only
var out = io.buffer();
var f = fs.open('copy.bin', 'r');
var n = io.copy(out, f); // 8192-char chunks
f.close();
io.writeString(out, 'done');io.buffer is the in-memory reader/writer (read/write/text/length/reset);
io.readAll is a convenience for small data, while io.copy streams. Pass
io.buffer where a format or helper expects a stream; fs.open handles work
directly. Format readers/writers never close or flush the underlying IO.
TOML documents use the same reader/writer contract:
use 'toml.fun';
var f = fs.open('config.toml', 'r');
var doc = toml.reader(f).read(); // nil after the first read
f.close();
var out = io.buffer();
toml.writer(out).write(doc);Tables are sets, arrays are lists, and values keep TOML scalars. Date/time,
inf and nan values are TomlLiteral objects (.kind / .text) so the
writer can reproduce their TOML syntax. The reader returns one document and
then nil; the writer writes one document. Neither closes the handle.
CSV files stream row by row. The reader and writer wrap a file handle and do not close it:
use 'csv.fun';
var f = fs.open('users.csv', 'r');
var r = csv.reader(f);
var row = r.read(); // nil at EOF
while row <> nil do
...
row = r.read();
end do;
f.close();
var g = fs.open('out.csv', 'w');
var w = csv.writer(g);
w.write(['name', 'age']);
g.close();Fields are strings and columns are strict. A blank line is a one-empty-field
record by default, so it fails the column check in a multi-column file; pass
[skipEmptyLines: true] to ignore completely empty lines. The writer quotes
fields when needed and uses \n; pass [crlf: true] for CRLF. The reader
accepts any object with read(n), the writer any object with write(s).
Data can be validated against a JSON Schema. Schemas are written in FD by default, and JSON text or an already parsed value works too:
use 'jsonschema.fun';
var schema = 'user.schema.fd'.load();
var r = JsonSchema.check(data, schema);
if not r.ok then
?. r.first.path & ': ' & r.first.message;
end if;Errors carry a JSON Pointer path (/user/name, /tags/0). v0 covers type
(including lists), properties / required / additionalProperties,
items / minItems / maxItems, numeric bounds, minLength / maxLength
(counted in Unicode code points), pattern, enum and const; $ref,
composition keywords and tuple items are not supported yet.
Platform-split convention: the Windows implementation keeps the plain module name, and the Unix backend is
<name>-unx.fun(Linux + macOS); the older<name>-lnx.funname is kept as a thin wrapper on top of it. Logic shared by both lives in<name>-base.fun(not counted above). Backends that only work on Linux (e.g.lib-proc, which reads/proc) stay<name>-lnx. E.g.lib-os/lib-os-unx(+lib-os-lnx), andlib-unicode-base+lib-unicode/lib-unicode-unx. A neutral name works as well:useprobes the platform variants (macOS-darwin→-unx, Linux-lnx→-unx) and picks one when it exists.
fun/demo— syntax demos: functions, closures, currying, classes, objects, sets, regex, and exception handling.fun/demos— full applications and benchmarks: thenotepad--editor,odbc-search, and performance/assembly/JIT benchmarks.fun/demos/package-demo— a small package with a path dependency:tidy,run(project and flat mode), andpack.fun/demos/test-demo— a lib-only package with tests, a shared helper, and a failing-assertion example.fun/demos/http-cors-demo— CORS preflight, credentials, exposed headers, and a browser page.fun/demos/http-bearer-demo— Bearer token auth, 401 challenges, and a same-origin page.fun/demos/http-session-demo— cookie session login/logout, SID rotation, and server-side state.fun/demos/fs-demo— nested directories, listing, walking, atomic write, and cleanup.fun/demos/cli-demo— commands, flags, aliases, env, hooks, help/version, and testable output.
Fun is distributed under a dual-license model:
- Open-source (MIT) — see
LICENSE. You are free to use, modify, and redistribute the source, provided the copyright notice is retained. - Commercial license (additional terms that apply only to these closed-source redistribution cases):
- Substantially modifying the kernel and commercially distributing the result closed-source — see
LICENSE.custom; - Embedding
fun.dllin a closed-source commercial product or SaaS backend — seeLICENSE.dll.
- Substantially modifying the kernel and commercially distributing the result closed-source — see
In short: internal use, study/research, and releasing modified versions under an open-source license (MIT/GPL etc.) are all free. A commercial license is needed only when you sell a modified kernel or fun.dll as a closed-source commercial product. Commercial licenses are one-time, priced per project/company — contact <zwd@funlang.org>.
© 2010-2026 Zhang Weidong <zwd@funlang.org>