Scheme to OpenSCAD Function Compiler
Written by Dominik Pantůček on 2026-07-02
racketOpenSCAD as a 3D modelling tool supports functional programming and therefore it can be used to easily express many complex ideas. However some programming languages such as Scheme can be more convenient in which case it might be handy to have a compiler of such language into the target system.
Although the support for functional programming in OpenSCAD is rather limited, it already has support for anonymous first-class functions and named functions with recursion capability. The only limitation is that any function that is defined can have only one expression in its body - like the following example:
function func1(a, b, c) = a * b + c;
But with recursion and ternary operator (conditional evaluation), we can start doing interesting stuff. Consider the factorial function:
function fact(x) = x <= 1 ? 1 : x * fact(x - 1);
And given the fact, that functions are first-class citizens now, it is possible to create higher-order functions like polynomial evaluation:
function poly(a, b, c) =
function(x) a * x * x + b * x + c;
What do we miss here? For example we miss a simple let construct like in Scheme which would allow us to write more complex functions easily. Take the following procedure with a simple let form inside which computes a total area of a rectangular arrangement of tiles of specified size:
(define (func2 tile-side width-tiles height-tiles)
(let ((width (* tile-side width-tiles))
(height (* tile-side height-tiles)))
(* width height)))
We cannot directly transform this into OpenSCAD functions. Or can we? What if we transform the let form into a normal lambda form:
(define (func2 tile-side width-tiles height-tiles)
((lambda (width height)
(* width height))
(* tile-side width-tiles)
(* tile-side height-tiles)))
This is pretty standard expansion and it can readily be translated into OpenSCAD function:
function func2(tile_side, width_tiles, height_tiles) =
(function(width, height) width * height)(
tile_side * width_tiles,
tile_side * height_tiles);
This leads to a rather simple strategy for compiling Scheme code to OpenSCAD functions.
- Fully expand the code into simple lambdas and primitive procedures.
- Translate the lambdas into functions and function applications.
The first step has been solved many times, so let's have a look at the second step implemented in Racket. It takes a list argument with the symbolic representation of the fully-expanded code into lambdas and produces a string representing the same function implemented as OpenSCAD function:
(define (compile-scad-expr lst)
(match lst
; Lambda
((list 'lambda args expr)
(format "(function(~a)\n~a)"
(string-join (map format-scad-id args) ", ")
(indent-scad-code (compile-scad-expr expr) 1)))
; Conditional evaluation
((list 'if p t f)
(format "(~a\n ? ~a\n : ~a)"
(compile-scad-expr p)
(compile-scad-expr t)
(compile-scad-expr f)))
; N-ary infix operators
((list (and (or '/ '* '+ '-) fn) xs ...)
(format "(~a)"
(string-join
(map compile-scad-expr xs)
(symbol->string fn))))
; Binary infix operators
((list (and (or '> '< '>= '<=) fn) a b)
(format "(~a~a~a)"
(compile-scad-expr a)
fn
(compile-scad-expr b)))
((list '= a b)
(format "(~a == ~a)"
(compile-scad-expr a)
(compile-scad-expr b)))
; Application
((list (? symbol? fn) arg ...)
(format "~a(~a)"
(compile-scad-expr fn)
(string-join (map compile-scad-expr arg) ", ")))
((list fn arg ...)
(format "~a\n(~a)"
(compile-scad-expr fn)
(string-join (map compile-scad-expr arg) ", ")))
; Identifier
((? symbol? id)
(format-scad-id id))
; Number
((? number? num)
(format "~a" num))
; Unmatched
(_ (format "ERROR: >>~v<<" lst))))
As we can see, the automated conversion takes care of writing the impractical infix syntax of OpenSCAD and ensures dashes are converted to underscores. It makes one wonder why they didn't stick to S-expressions for such nice piece of software.
The indent-scad-code and format-scad-id auxiliary
procedures are rather simple:
(define (format-scad-id sym)
(string-replace
(format "~a" sym)
"-" "_"))
(define (indent-scad-code str (n 1))
(define ind (make-string n #\space))
(string-join
(for/list ((line (in-lines (open-input-string str))))
(format "~a~a" ind line))
"\n"))
And that's about it! Now you can write arbitrary functions in OpenSCAD using a nice subset of Scheme. And that is always an improvement.
Hope you liked our venture into transpiling functional code between languates and see ya next time for more!