Skip to content
funlangPublic

About

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.

Topics

Resources

Contributing

Stars

24 stars

Watchers

3 watching

Forks

Latest commit

 

History

536 Commits

Folders and files

Repository files navigation

Fun Language

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: 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).


Table of contents


Why Fun

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.


Core features

  • 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' (optionally as 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.exe and embeddable runtime fun.dll exporting a simple Run interface.
  • Self-hosted standard library — 79 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.

Quick start

Prebuilt binaries

Prebuilt interpreters are attached to GitHub Releases:

  • Linux x64 / arm64: .tar.gz, with libtcc.so and the standard lib/;
  • macOS x64 / arm64: .zip. Both archives include libtcc.dylib. These builds are unsigned and not notarized; use the bundled install.sh --local (curl does not set the quarantine attribute) or run xattr -dr com.apple.quarantine <dir> after extracting;
  • Windows x64: .7z. The build is unsigned, so SmartScreen may warn; extract with 7-Zip and double-click install.cmd (or run powershell -ExecutionPolicy Bypass -File install.ps1). The archive ships fun.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.json records 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 | sh

If 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):

  1. Extract the .7z archive with 7-Zip.
  2. Double-click install.cmd in 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

Packages

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>] and remove <name> edit only the dependencies set; versions are exact tags, branches, or commits.
  • fun package tidy installs the declared revisions into the shared ~/.fun cache (FUN_HOME overrides it), verifies git tree hashes, and writes the resolved block. Path dependencies are used in place; only git dependencies need git.
  • fun run [args...] runs the manifest bin from 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 pack embeds 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.


Testing

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 broad try/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 prints todo passed: ... (remove todo). Todos are counted separately and do not fail the run.
  • test.fun, the globals test/assert/runTests, the aliases as test / as assert, and dependency keys test/assert are 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.


A taste of Fun

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.


The capability ladder

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 #!asm block above is 32-bit x86 (Windows) assembly (esp-relative args, ecx). The #!C path is portable, but the assembly snippet is architecture-specific — on Linux/ARM use the C form or rewrite the assembly for the target ABI.


The FD data format

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., +1 and 1e are 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 as toStr, so 0.1 + 0.2 is written as 0.3. Tiny floating-point round-trip differences are accepted for readability.
  • Only true, false and null are literals. yes/no are 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:= and key=value).
  • Duplicate named keys are last-wins, matching JSON.parse.

Foreign function interface

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 -> int

The 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).


Keyword aliasing

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.


Embedding with fun.dll

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.dll in a closed-source commercial product (exe/dmg/apk or a SaaS backend) requires a commercial license — see LICENSE.dll.

Repository layout

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

Building

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.

Platform capability matrix

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.

Cross builds (macOS and Linux aarch64, from Linux x86_64)

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, curl or wget, 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, or osxcross is required — Zig ships a libSystem stub.
  • bootstrap-linux-aarch64.sh reuses the ppcrossa64 and 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.dll and ffi.dll). The cross-built Linux aarch64 artifact is the core interpreter (its -dRegex and -dLinuxFFI need 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.

Third-party dependencies (not carried in the repo)

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.sh builds 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 (-dRegex off); no TCC -> no libtcc.so, so lib-tcc is unavailable. Neither affects the core interpreter.
  • Convenience fetcher: src/prj/fun/fetch-3rd-party.sh downloads and unpacks the pinned PCRE 8.45 and TCC 0.9.27 releases into src/3rd/ (used by CI).

Regression tests and 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 counterpart

The 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 strict

funcmd-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.


Standard library

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 for fun 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');

Command-line programs

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.fun name 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), and lib-unicode-base + lib-unicode / lib-unicode-unx. A neutral name works as well: use probes the platform variants (macOS -darwin→-unx, Linux -lnx→-unx) and picks one when it exists.


Examples

  • fun/demo — syntax demos: functions, closures, currying, classes, objects, sets, regex, and exception handling.
  • fun/demos — full applications and benchmarks: the notepad-- 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), and pack.
  • 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.

License

Fun is distributed under a dual-license model:

  1. Open-source (MIT) — see LICENSE. You are free to use, modify, and redistribute the source, provided the copyright notice is retained.
  2. 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.dll in a closed-source commercial product or SaaS backend — see LICENSE.dll.

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>

About

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.

Topics

Resources

Contributing

Stars

24 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages