a small, fast scripting language and bytecode vm written in portable c11.
flint is deliberately small.
no llvm. no cmake. no configure script. no dependency jungle.
just c11, libc, libm, a bytecode vm, and enough language to get things done.
make release
./flint -e 'print(1 + 2)'- portable c11
- stack-based bytecode vm
- 64-bit nan-boxed values
- single-pass pratt parser
- mark-and-sweep garbage collector
- closures with captured locals
- lists, tables, strings and modules
- bytecode verifier
- computed-goto interpreter
- standard library with os, process and http
- human and json diagnostics
flint syncfor updating the library- no runtime dependencies beyond libc and libm
the implementation is small enough to read.
that's intentional. large codebases have enough fans already.
to build from source:
git clone https://github.com/programmersd21/flint
cd flint
make releasethe binary is ./flint.
to install flint to your path:
make installby default, this installs the binary to ~/.local/bin and the standard library to ~/.flint/stdlib.
for packaging, override the installation paths:
make install PREFIX=/usr/local LIBDIR=/usr/local/share/flint/libto uninstall:
make uninstallflint requires no runtime dependencies beyond libc and libm. the installation consists of one binary and fourteen .fl files.
flint searches for its standard library in this order:
$FLINT_STDLIB, an explicit override.<executable>/lib, beside the binary.~/.flint/stdlib, the default installation directory.
the third location is necessary because a binary on PATH does not necessarily have a lib/ directory beside it.
if you move the binary somewhere without an adjacent lib/ directory, keep the standard library in ~/.flint/stdlib or set FLINT_STDLIB.
fn counter() {
let n = 0
fn next() {
n += 1
return n
}
return next
}
let c = counter()
print(c())
print(c())
output:
1
2
modules are files, each with its own namespace.
shapes.fl:
let tax = 0.2
export fn taxed(amount) {
return amount * (1 + tax)
}
main.fl:
import "shapes.fl"
print(shapes.taxed(10))
print(shapes.tax)
output:
12
nil
tax is private to shapes.fl. only exported names are accessible through the module namespace.
two modules can define the same private name without exposing or overwriting each other's values.
| module | purpose |
|---|---|
math |
math functions and constants |
random |
random numbers and shuffling |
time |
clocks and time utilities |
fs |
files and directories |
path |
path manipulation |
collections |
collection helpers |
json |
parsing and serialization |
os |
information about the host system |
process |
spawning programs and capturing output |
http |
http client requests |
args |
script arguments and flags |
encoding |
hex, base64 and url encoding |
ansi |
terminal escape codes |
pretty_print |
multi-line value rendering |
the library is written in flint wherever possible.
no package manager. no registry. no twelve-layer dependency tree to print a number.
flint sync downloads the standard library from the repository and installs it into ~/.flint/stdlib.
each file is compiled before installation. a failed download, truncated response or 404 leaves the existing files untouched.
use --dry-run to preview changes or --ref to select a release tag instead of main:
flint sync --dry-run
flint sync
flint sync --ref=v0.7.0source
│
▼
pratt parser
│
▼
bytecode
│
▼
verifier
│
▼
stack vm
the parser emits bytecode directly.
values fit in 64 bits. closures capture locals. memory is managed by a mark-and-sweep garbage collector.
there is no giant intermediate representation sitting around because apparently compilers enjoy paperwork.
the default interpreter uses a portable c11 dispatch loop.
on compilers that support computed goto, build the alternative interpreter:
make flint-gotobenchmark both implementations:
make bench
make bench-gotoresults are documented in bench/RESULTS.md.
measurements are preferred over benchmark folklore. the cpu has already suffered enough.
flint --check program.fl
flint --error-format=human program.fl
flint --error-format=json program.fl
flint --dump-bytecode program.fldiagnostics include source locations and stable error codes.
the verifier checks generated bytecode before execution, including opcodes, operands, indices and jump targets.
executing invalid bytecode and hoping for the best is not a runtime strategy.
run the main checks:
make check
make stressmake checkperforms a clean release build and runs the test suites.make stressruns with aggressive garbage collection under asan and ubsan.
other useful targets:
make debug
make test
make unit
make diagnostic-test
make bench
make lint
make fmt
make fmt-checkif make stress finds something, congratulations. the garbage collector has opinions.
docs/language.md- language referencedocs/syntax.md- syntax and expressionsdocs/functions.md- functions and closuresdocs/data.md- strings, lists and tablesdocs/modules.md- modulesdocs/library.md- standard librarydocs/diagnostics.md- diagnosticsdocs/internals.md- vm internalsARCHITECTURE.md- architectureSPEC.md- language specificationexamples/- examples
keep changes focused.
if you find a bug, add a test.
before opening a pull request, run:
make check
make stresssee CONTRIBUTING.md.
if a patch adds a dependency, explain why c could not do the job.
flint is licensed under the mit license. see LICENSE.
