Skip to content

Repository files navigation

flint

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)'

features

  • 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
  • human and json diagnostics
  • no runtime dependencies beyond libc and libm

the implementation is small enough to read.

that's intentional. large codebases have enough fans already.

install

to build in place:

git clone https://github.com/programmersd21/flint
cd flint
make release

the binary is ./flint.

to install, so flint is on your PATH:

make install

which puts the binary in ~/.local/bin and the standard library in ~/.flint/stdlib. PREFIX, BINDIR, LIBDIR and DESTDIR all override that, for packaging:

make install PREFIX=/usr/local LIBDIR=/usr/local/share/flint/lib

there is nothing to uninstall beyond make uninstall -- no runtime, no shared libraries, no configuration, no build directory left behind. the whole thing is one binary and seven .fl files.

flint searches for its standard library in this order:

$FLINT_STDLIB                  an explicit override, wins over everything
<executable>/lib               beside the binary
~/.flint/stdlib                where `make install` puts it

the third entry is what make install relies on, because a binary on PATH has no lib/ beside it. if you move the binary somewhere without lib/ next to it, either keep ~/.flint/stdlib or set FLINT_STDLIB.

example

fn counter() {
    let n = 0

    fn next() {
        n += 1
        return n
    }

    return next
}

let c = counter()

print(c())
print(c())
1
2

modules are just files, and each one has its own namespace:

# shapes.fl
let tax = 0.2                     # private to this file

export fn taxed(amount) {         # public
    return amount * (1 + tax)
}
# main.fl
import "shapes.fl"

print(shapes.taxed(10))          # 12
print(shapes.tax)                 # nil -- a private name is not there

two modules can both have a private tax and neither can see or overwrite the other's, which was not true before 0.6.0.

standard library

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

the library is plain flint code where possible.

no package manager. no registry. no twelve-layer dependency tree to print a number.

vm

source
  │
  ▼
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 collector.

there is no giant intermediate representation sitting around because apparently compilers enjoy paperwork.

performance

the default interpreter uses a portable C11 dispatch loop.

for compilers that support computed goto:

make flint-goto

benchmark both:

make bench
make bench-goto

results are kept in bench/RESULTS.md.

measurements are preferred over benchmark folklore. the cpu has already suffered enough.

diagnostics

flint --check program.fl
flint --error-format=human program.fl
flint --error-format=json program.fl
flint --dump-bytecode program.fl

diagnostics include source locations and stable error codes.

the verifier checks generated bytecode before execution, including opcodes, operands, indices and jump targets.

because executing invalid bytecode and hoping for the best is not a runtime strategy.

development

make check
make stress

make check performs a clean release build and runs the test suites.

make stress runs 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-check

if make stress finds something, congratulations. the garbage collector has opinions.

documentation

contributing

keep changes focused.

if you find a bug, add a test.

before opening a pull request:

make check
make stress

see CONTRIBUTING.md.

if a patch adds a dependency, explain why c could not do the job.

license

MIT. see LICENSE.

About

a tiny scripting language. fast to start, easy to read, built for the shell.

Topics

Resources

Contributing

Stars

6 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Contributors

Languages