Making Reduct (previously SCON), a language for scripting and configuration within PatchworkOS, and accidentally faster than Lua, now available as its own project. #118
Replies: 1 comment 2 replies
|
I have no opinion on most of this as I haven't looked in detail, though at first glance it looks reasonable. Handles and tries look good. (There might be an optimization opportunity for special-casing short lists, I dunno.) And kinda impressive to do all of it in a header library. But... I don't get the infix syntax on Reduct. You're adding a completely new notation (infix) which means a new tokenizer/parser, you lose some of lisp's core features (homoiconicity, everything-is-a-list), it's additional syntax you have to maintain, and it's something extra for the user to have to learn. All to get rid of some parentheses aka deep nesting. (Which I acknowledge is a worthy goal.) Admittedly I don't know the background reasoning for your decision, so I might well be missing something obvious. But, I'd like to offer a suggestion: let newlines be significant whitespace. I propose two possible syntaxes, both of which compile in my head (in other words, fair warning...): Option ABoth bindings and expressions can be newline-delimited. Expressions must be enclosed by parens. An equivalent to the above: Option BBoth bindings and expressions can be newline-delimited. Bindings use An equivalent to the above: (BTW I have no idea what the Both of these styles still support parentheses so you can write e.g. You lose a lot of parens, keep the simplicity of the grammar, keep homoiconicity. Any of this resonate with you? |
Uh oh!
There was an error while loading. Please reload this page.
So I got distracted again. I've been working on the new desktop interface for PatchworkOS; what I noticed rather early is that to create a modern looking UI, a pure, C-based API is simply not good enough, writing even a basic UI becomes a pain.
Why?
The obvious solution was to create a minimal programming language that could act as an HTML/CSS style markup language. Since SCON was already being used for component manifests, adding a few more features to it was another obvious choice.
What?
As part of this process I was reading up on language design and came across too many good ideas that I simply had to add. Eventually, I made the decision to turn SCON into its own project, which was renamed to Reduct.
Reduct is a functional, immutable, S-expression based configuration and scripting language. It aims to combine the flexibility of a Lisp with the ease-of-use and performance of a language like Lua. All within a C99 header-only library.
If you are curious and want to know more, then feel free to check out the GitHub! But I will go over some stuff in this post as well.
Syntax
Lisps, which Reduct takes heavy inspiration from, are generally agreed to be a very powerful style of language and yet are frequently criticized for their poor readability.
The most often blamed source of this poor readability is the sheer volume of parentheses.
Reduct makes the argument that most of these complaints are due to nesting, not parentheses; that it can be solved via infix notation, banning
letand a more modern style guide.Included are three examples of a basic program written in common Lisp C, and Reduct.
Lisp
C
Reduct
Note how in Reduct the use of curly braces for infix notation and the
defintrinsic for scoped definitions allows for a more familiar, imperative-like structure while remaining entirely functional, and S-expression based.How?
Reduct is implemented as a register-based bytecode language, where the Reduct source is first parsed into an Abstract Syntax Tree (AST) and then compiled into a custom bytecode format before being executed by the virtual machine/evaluator.
The bytecode format itself is a stream of 32bit instructions, with all instructions able to read/write to an array of registers, or read from an array of constants.
See inst.h for more information on instructions.
Since Reduct is immutable, the constants array is also used for "captured" values from outer scopes (closures) and we can also allow the compiler to fold constant expressions at compile-time, far more than would normally be possible.
See compile.h for more information on the compiler.
To improve caching and reduce pointer indirection, Reduct uses "handles" (
reduct_handle_t) which are Tagged Pointers using NaN boxing to allow a single 64bit value to store either a 48 bit signed integer, IEEE 754 double or a pointer to a heap allocated item.See handle.h for more information on handles.
Items (
reduct_item_t) represent all heap allocated objects, such as lists, atoms and closures. All items are exactly 64 bytes in size and allocated using a custom pool allocator and freed using a garbage collector and free list.Since Reduct uses its handles to store most integers and floats, it can avoid heap allocations for many common values, significantly reducing the pressure on the garbage collector and improving caching.
See item.h for more information on items.
Lists are implemented as a "bit-mapped vector trie", providing$O(log_{w} n)$ access, insertion, and deletion, where $w$ is the width of each node in the trie.
See list.h for more information on lists.
All atoms use String Interning, meaning that every unique atom is only stored once in memory. This makes any string comparison into a single pointer comparison, and it means that parsing the integer/floating point value of an atom or an items truthiness only needs to be done once.
See atom.h for more information on atoms.
Many additional optimization techniques are used, for example, Computed Gotos, setjmp based error handling to avoid excessive error checking in the hot path, Tail Call Optimization and much more.
See eval.h for more information on the evaluator.
Benchmarks
Included below are a handful of benchmarks comparing Reduct with python 3.14.3 and Lua 5.4.8 using hyperfine, all benchmarks were performed in Fedora 43 (6.19.11-200.fc43.x86_64).
Fib35
Finds the 35th Fibonacci number without tail call optimization.
reduct bench/fib35.rdtlua bench/fib35.luapython bench/fib35.pyFor this benchmark, memory usage was also tracked using
heaptrack:reduct bench/fib35.rdtlua bench/fib35.luapython bench/fib35.pyFib65
Finds the 65th Fibonacci number with tail call optimization.
reduct bench/fib65.rdtlua bench/fib65.luapython bench/fib65.pyBrainfuck
A simple jump-table optimized Brainfuck interpreter that runs a "Hello World!" program.
reduct bench/brainfuck.rdtlua bench/brainfuck.luaFor this benchmark, memory usage was also tracked using
heaptrack:reduct bench/brainfuck.rdtlua bench/brainfuck.luaMandelbrot
Outputs an 80 by 40 visualization of the Mandelbrot set with 10000 iterations.
reduct bench/mandelbrot.rdtlua bench/mandelbrot.luaSee the Benchmarks for more information.
There is still plenty of room for further improvement, as always, I'd gladly hear any suggestions or issues that anyone may have!
All reactions