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.5
- Website: https://funlang.org
- Author: Zhang Weidong <zwd@funlang.org>
- License: MIT, plus commercial terms (see License)
- Status and roadmap: ROADMAP.md
Version 9.5.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);
- 79 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; an alias also exposes names imported by forwarding modules, and 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 — 79 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.
Prebuilt interpreters are attached to GitHub Releases:
- Linux x64 / arm64:
.tar.gz, withlibtcc.soand the standardlib/; - macOS x64 / arm64:
.zip. Both archives includelibtcc.dylib. These builds are unsigned and not notarized; use the bundledinstall.sh --local(curl does not set the quarantine attribute) or runxattr -dr com.apple.quarantine <dir>after extracting; - Windows x64:
.7z. The build is unsigned, so SmartScreen may warn; extract with 7-Zip and double-clickinstall.cmd(or runpowershell -ExecutionPolicy Bypass -File install.ps1). The archive shipsfun.exe,fun-safe.exe, the installers, and the x86-64 runtime DLLs (pcre3.dll,ffi.dll,libtcc.dll,zlib.dll) that the CI built;capabilities.jsonrecords anything a particular build could not provide.
macOS / Linux one-liner (verifies SHA256SUMS, installs under ~/.fun):
curl -fsSL https://raw.githubusercontent.com/funlang/fun/main/install.sh | shIf the latest stable release has no binaries yet, the installer says so and
falls back to the newest nightly. Pass --tag or --nightly to choose
explicitly. The default install links fun into ~/.local/bin; when that
directory is not on PATH, add --add-path to append a marker-delimited
block to ~/.profile (or ~/.zshrc for zsh), or pass --bin-dir DIR to put
the links somewhere already on PATH.
Windows install (per-user, no administrator rights):
- Extract the
.7zarchive with 7-Zip. - Double-click
install.cmdin the extracted folder.
install.cmd copies the tree to %LOCALAPPDATA%\Programs\Fun, adds
%LOCALAPPDATA%\Programs\Fun\fun to the user PATH, registers .fun,
.foo and .fxx with a Fun Script ProgID (so double-clicking a script
runs it), adds a Win+R fun entry, and appears in Settings > Apps for
uninstalling. Useful options: -AllUsers (elevated, under Program Files),
-Dir DIR, -NoPath, -NoAssoc, -Portable. Run uninstall.cmd from the
install directory, or use Settings > Apps, to remove it. Windows may keep
an existing per-user choice for an extension; use Open with > Choose
another app once to select Fun.
Each archive contains a fun/ runtime tree: fun/fun (fun/fun.exe on
Windows) and fun/fun-safe (the capability-restricted build), plus lib/,
demo/, demos/ and the regression suite in test/. Run
./fun/fun --help, or run the shipped suite with
./fun/test/run-linux-regression.sh on Linux/macOS and
fun\test\run-windows-regression.bat --portable on Windows.
install.sh is included in every Unix archive (not only macOS) and also
creates funcmd and funcmd-safe compatibility symlinks. Both installers are
packaged next to the runtime tree and are the supported way to get a global
fun command. Run ./install.sh --local from an extracted archive to install
that exact tree offline; plain ./install.sh downloads a release.
Every release also carries capabilities.json and SHA256SUMS; optional
features that were unavailable are listed there.
Linux artifacts are built on Ubuntu 24.04, so they need glibc 2.34 or newer;
when getapi/FFI is enabled they also need libffi.so.8 at run time.
Nightly builds are prereleases, so releases/latest stays on the stable
version. fun.dll is a separate licensed product and is not part of these
archives.
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.5.5',
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.skip('reason')inside a test body ends that test as skipped; the reason is optional, skips are counted separately, and they do not fail the run. Do not catch it in a broadtry/except: the test is still reported as skipped, but the rest of the body continues.test.todo('name', 'reason')registers a placeholder that is not run;test.todo('name', { ... }, 'reason')runs the body and treats a failure as expected. A todo body that passes printstodo passed: ... (remove todo). Todos are counted separately and do not fail the run.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/.dylib) |
| 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, libtcc.so on Linux and libtcc.dylib on macOS x64, 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).
A bare var x argument for a p parameter is an OUT/INOUT pointer on both backends: the engine passes a pointer-width cell and copies the result back into x. For explicit control (or for a caller-owned buffer) pass the address yourself with buf.toNum(-1).
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, 79 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.
Three capability layers (regex, FFI, runtime C) plus the machine-code layer, and a second build mode for untrusted scripts:
| Platform | regex | FFI | runtime C (tcc) | asm | safe build |
|---|---|---|---|---|---|
| linux-arm64 | yes | yes | yes | yes | yes |
| linux-x64 | yes | yes | yes | yes | yes |
| macos-arm64 | yes | yes | yes | yes | yes |
| macos-x64 | yes | yes | yes | yes | yes |
| win-x64 | yes | yes | yes | yes | yes |
safe build is the separate fun-safe binary (-dFunSafe, strict level); it
ships next to the default interpreter, so the default build and its behaviour
are unchanged. Every other column describes the default binary. runtime C is
now available on both macOS architectures through libtcc.dylib. Apple
Silicon uses a small TCC patch for the Darwin ucontext fields and MAP_JIT
handling. The release notes and each archive's capabilities.json are
generated from probes, so a degraded build cannot claim a capability it does
not have.
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 cross-built Linux aarch64 artifact is the core interpreter (its-dRegexand-dLinuxFFIneed a libc / libffi that are not cross-built yet); a native Linux arm64 build, as in CI, has both. - 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; build-win64-deps.sh produces the x86-64 libtcc.dll (win64); build-tcc-darwin.sh produces libtcc.dylib (macOS x86_64 and arm64, the latter with patches/tcc-darwin-arm64.patch) |
| 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 |
| zlib 1.2.11 (win64) | src/3rd/zlib-1.2.11/ |
https://zlib.net/fossils/ | build-win64-deps.sh produces the x86-64 zlib.dll; Linux/macOS use the system zlib |
- 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.
The Windows job also runs build-win64-deps.sh (x86-64 libtcc.dll and
zlib.dll), probes both with a real compile/deflate call, builds
funcmd-safe.exe with the same feature set, and runs the focused safe test.
The stage step copies only what those probes accepted.
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 tests
# Windows
cd src\prj\fun && set SAFE=strict && make-win64.bat # produces funcmd-safe.exe
fun\test\run-safe-regression.bat strictfuncmd-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. Every release also ships
the strict build; a separate linux-safe CI job verifies strict and readonly,
and the Windows job builds and runs the same strict capability test. The
linux-safe job only runs on manual dispatch (safe_only), not on push/PR.
The standard library in fun/lib is written in pure Fun (79 modules):
In total roughly 6,000 lines of logical code (excluding comments) — all 79 modules written in Fun itself.
- Data:
lib-json,csv,toml,jsonschema,lib-yaml,lib-xml,lib-base64,lib-cstruct,lib-md5,lib-crypt,lib-hmac,lib-url - 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,process,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-multipart,http-session,jwt - 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.
Uploaded forms are read from the request directly:
var m = req.multipart();
var name = m.fields['name'][0];
var file = m.files['avatar'][0];HttpMultipartWriter builds the same bodies for client uploads:
use 'http-multipart.fun';
var w = HttpMultipartWriter();
w.field('name', 'alice');
w.file('avatar', 'a.txt', data, [contentType: 'text/plain']);
var body = w.body();
var ct = w.contentType();Pass body and Content-Type: <ct> to HttpClient to send it.
Cookies can be signed so tampering is rejected:
res.signedCookie('uid', 'u1', secret, [path: '/', httpOnly: true]);
var r = req.signedCookie('uid', secret);
if r.ok then ?. r.value; end if;JWT tokens use the same auth helpers:
use 'jwt.fun';
var token = jwt.sign([sub: 'u1', exp: jwt.now() + 3600], secret);
var r = jwt.verify(token, secret, [issuer: 'app']);
if r.ok then ?. r.claims['sub']; end if;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, err and input.
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
streams ctx.out(), ctx.err() and ctx.stdin(). 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.
The optional input is any object with read(n), using the same reader contract
as io.fun. It defaults lazily to io.stdin(), so a command that reads
ctx.stdin() accepts piped input without extra setup. A child's own input
overrides the root's; otherwise it inherits. When a child inherits, a parent hook
and the child action share the same input stream, so a hook that reads consumes
input before the action. The framework never reads, buffers or closes the
reader, and help/version do not consume it. Tests use io.buffer for both
output and input:
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;Input works the same way:
var source = io.buffer('hello');
var copied = io.buffer();
fun copy(ctx)
ctx.out().write(io.readAll(ctx.stdin()));
end fun;
var cat = cli.command('cat', [input: source, out: copied]);
cat.action(copy);
?. cat.run([]) = 0;
?. copied.text() = 'hello';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.
io.scanner walks any reader token by token, by default one line at a time
(\n or \r\n; the final line may omit the newline). scan() returns false
at EOF or on error; text() is the current token and err() reports
token too long or a reader error. maxToken defaults to 64 KB, and
delimiter replaces the newline:
use 'io.fun';
var sc = io.scanner(io.stdin());
while sc.scan() do ?. sc.text(); end do;
if sc.err() <> nil then raise sc.err(); end if;Use io.scanner(r, [delimiter: ',']) for a custom separator; the scanner never
closes the reader and may read ahead, so do not mix it with direct read().
Process stdio uses the same contract:
use 'io.fun';
var input = io.stdin();
var output = io.stdout();
var errors = io.stderr();
var chunk = input.read(4096); // may be short; '' means EOF
output.write('received ' & chunk.length() & 10.toChar());
errors.write('warning' & 10.toChar());io.stdin is a Reader and io.stdout/io.stderr are Writers. They do not
close, flush or translate line endings, and they do not create pipes; io.pipe
creates pipes separately, and CLI stdin remains a later layer. Process stdio
treats strings as byte strings: on UTF-16 Windows builds each character is
written as one byte, so
ASCII and binary data round-trip unchanged; non-ASCII text must be encoded by
the caller first. Windows console code pages and UTF-16 are not translated.
Process stdio uses getapi, so an FFI-enabled build is required: io.buffer
still works in safe or FFI-less builds, but reading or writing process stdio
raises an error there. A closed pipe can terminate the process through SIGPIPE
on Unix, as usual. Do not mix io.stdin() with the built-in line/char/all
input helpers on the same stdin, and do not mix io.stdout() with ?. when
output order matters: they use independent buffering/FFI paths.
Process pipes use the same reader/writer contract:
use 'io.fun';
var p = io.pipe();
p.writer.write('hello');
p.writer.close(); // reader sees EOF after buffered data
var text = io.readAll(p.reader); // 'hello'
p.close();io.pipe() creates a blocking OS pipe; p.reader and p.writer are Reader and
Writer plus an explicit close(), and p.close() closes both ends. Closing the
writer lets the reader drain buffered data and then return ''; closing the
reader makes later reads raise; a later write may terminate the process with
SIGPIPE on Unix (not a catchable error).
p.close() closes both ends and discards unread data.
Pipes own their handles and must be closed explicitly; io.readAll/io.copy
never close them. Pipe buffers are finite: writing more than the OS buffer
before reading deadlocks a single-threaded script. v1 is for small payloads
only; there is no supported thread/process hand-off yet, and larger transfers
need a future streaming/process-handoff layer. Reads (including readAll and copy)
block while the pipe is empty and the writer is open; there is no timeout or
cancel. Close the writer to make an empty read finish with EOF. The pair is not
thread-safe: do not read, write or close it concurrently. io.pipe needs an
FFI-enabled build and uses the same byte rules as process stdio.
Run a child process with process.run:
use 'process.fun';
var r = process.run('git', ['status', '--short'], [timeout: 5000]);
if r.ok then
?. r.stdout;
else
?. r.stderr;
?. r.exitCode;
end if;process.run(name[, args[, opts]]) executes the command directly, without a
shell. Trailing arguments are optional; pass nil or an empty list for no
arguments. Options are stdin (a string; default ''), cwd (empty
inherits the current directory), and timeout in milliseconds (nil/0
means no timeout), and output in bytes (nil/0 means unlimited).
The returned object has ok, exitCode, signal, timedOut, outputExceeded,
stdout, and stderr. A non-zero exit is a normal result; a command that
cannot start raises.
On Unix a signal exit reports a negative exitCode (for example -15) and the
signal number; Windows reports the process exit code and signal is nil.
stdout and stderr are captured separately as raw bytes; input/output are not
encoded or line-ending translated.
The call blocks until the child exits or the timeout expires. On timeout the
direct child is terminated and waited for, and the result has timedOut = true
and ok = false. The implementation uses a single-threaded stdio pump, so
stdout and stderr cannot deadlock each other; it does not create language
threads, does not kill the whole process tree, and does not provide streaming
or interactive stdio yet. When output is set, stdout and
stderr share one byte budget; exceeding it terminates the direct child and
sets outputExceeded = true and ok = false. Use sh -c / cmd /c explicitly
when shell syntax is required. process.run needs an FFI-enabled build. On Windows
v1 stdin is backed by a temporary file (Unix uses a pipe), and the ANSI
CreateProcessA path does not reliably handle non-ASCII command lines or paths;
the 32-bit Unicode/Delphi path remains manual. If a descendant keeps
stdout/stderr open after the direct child exits, v1 may stop reading after a
short grace period, so late descendant output can be lost.
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). Text input can be
parsed first with JsonSchema.checkJson(text, schema) or
JsonSchema.checkFd(text, schema); validJson / validFd are the boolean
shortcuts, and a parse failure is reported as a normal result with keyword
parse.
The v0.x subset covers type (including lists), properties / required /
additionalProperties, items (single schema or tuple) / additionalItems /
minItems / maxItems / uniqueItems, numeric bounds, minLength /
maxLength (counted in Unicode code points), pattern /
patternProperties / propertyNames, enum, const, allOf / anyOf /
oneOf / not, same-document $ref with $defs / definitions, and
format for email, date, time, date-time, uri, uri-reference,
uuid, ipv4, hostname and regex. External $ref, dependencies,
if / then / else and contains 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>