Lane is a small DSL for signed distance field (SDF) scenes. It typechecks Lane source and emits GLSL containing the scene SDF, gradient helpers, and only the support code needed by the program.
Install Rust with Cargo first. From this repository:
cargo build --workspace
cargo test --workspaceInstall or rebuild the compiler CLI, LSP server, and bundled Tree-sitter parser:
scripts/install-laneThe installer runs cargo install --path crates/lane-cli, cargo install --path crates/lane-lsp,
and rebuilds tree-sitter-lane/parser.so from the generated parser sources. If
the tree-sitter CLI is available, it regenerates the parser sources first.
Install the compiler CLI by itself:
cargo install --path crates/lane-cliInstall the LSP server by itself:
cargo install --path crates/lane-lspDuring development:
cargo run -- test.lane
cargo run -- test.lane test.glsl
cargo run -p lane-lspFixture tests live under tests/fixtures/. Add .lane files to
tests/fixtures/compile-pass/ when the only requirement is successful
compilation. Add matching .lane and .glsl files to
tests/fixtures/glsl-compare/ when generated GLSL should be compared with a
ground-truth file. The GLSL comparison ignores declaration order and allows
renaming generated functions or variables by adding or removing a single leading
underscore.
lane compiles lane source files into GLSL.
Usage:
lane [SOURCE [TARGET]] [--show]
lane SOURCE [--frag=FRAG] [--vert=VERT] [--version=VERSION] [--target=opengl|vulkan]
lane SOURCE [--frag-spv=SPV] [--vert-spv=SPV]
lane repl
lane preview SOURCE
lane list [NAME]
lane list 2d
lane list 3d
lane list all
lane -pc, --print-completion <bash|zsh|fish>
lane -h, --help
lane [SOURCE]compiles Lane to GLSL on stdout. WithoutSOURCE, Lane opens the interactive shell when stdin is a terminal and reads source from stdin otherwise.lane replopens the interactive shell explicitly. The shell accumulates valid Lane declarations, rejects#module, and emits GLSL when a submitted line is aconstdeclaration. After the first emission, laterconstlines show only GLSL lines added since the previous emission, including support structs inserted before older output. The REPL displays submitted Lane code, REPL messages, generated GLSL, and the current input linearly in one bottom-anchored, consistently padded transcript, with one character of inner left padding and different background colors for user code and output code. The current input block leaves one blank terminal row above and below it, and empty input shows gray placeholder text. Submitted Lane entries include source line numbers in their transcript gutter, and generated GLSL entries include generated line numbers in the same gutter style. The current input remains unnumbered and widens its blank gutter to stay aligned to the same source column as line numbers grow. Matching completions appear as gray inline hint text after the current token without changing the submitted input. Adjacent submitted Lane entries render inside one shared feed box only when no generated GLSL block separates them, and adjacent generated GLSL output renders as one continuous block only when the original transcript entries are truly adjacent; split mode keeps blank rows where hidden opposite-pane entries separate visible blocks. Oversized generated GLSL blocks are clipped to the available transcript pane instead of being hidden until the whole block fits. Errors are decided after submission: only the submitted Lane block for that failed submission is marked red, and the error message is shown above that code inside the same red block with aligned left padding. Failing submitted code uses an error marker in the gutter instead of source line numbers, and inline REPL error messages omit compiler line prefixes. Shell commands are recognized only at the start of a line and render as plain one-line messages (the initial welcome banner also renders as a plain line without box margins) that stay attached to their command response while leaving one blank row before the next submitted code block. REPL commands ignore trailing spaces:/help,/help, and similar forms behave the same. Command lines and error boxes keep a one-row vertical gap./helpprints REPL command help,/infoshows loaded modules, used directives, and provided objects,/showopens a native Vulkan preview window for the current session (preview failures are shown as REPL error blocks without exiting the shell),/splittoggles a split view where submitted Lane code and generated GLSL are rendered in separate panes with independent mouse-wheel scrolling and without adding toggle messages to the transcript,/clearclears the transcript but keeps the session,/codeshows the full session source as one code block,/save <filename>writes that source to a file,/export <filename>writes generated GLSL to a file,/restartstarts from an empty session, and/exitleaves the shell. Toggling/splitoff restores the full linear transcript, including generated GLSL chunks. Clicking a submitted Lane entry or its generated GLSL highlights both parts of that submission with a consistent selected background, right-clicking a transcript block copies that block's visible text to the terminal clipboard, including the error message on failed submitted Lane blocks, and PageUp/PageDown or the mouse wheel scrolls transcript rows rather than jumping by visual feed block. Enter submits the current input, Shift-Enter (or Alt-Enter fallback) inserts a newline when supported by the terminal, Up and Down recall submitted input history across sessions, Left and Right move through the current input, Tab completes to the longest unambiguous prefix using thelane-lspcompletion catalogue for Lane source and REPL command items for slash commands, Ctrl-F formats the current input, and Ctrl-C exits.lane SOURCE TARGETwrites generated GLSL toTARGET.lane --show SOURCE TARGET,lane -s SOURCE TARGET, orlane SOURCE TARGET --showwritesTARGETand prints the GLSL.lane SOURCE --frag=PATH --vert=PATHwrites complete preview fragment and vertex shaders.--target=vulkanemits Vulkan GLSL.- Auto-generated preview shading (when
mainis not explicitly defined) needs a scene object and material lookup: defineconst Object scene = ...and defineconst Hom(R3, Material) scene_material = .... When these requirements are missing, preview generation reports a clear requirement error instead of failing with a lower-level unknown-function message. lane SOURCE --frag-spv=PATH --vert-spv=PATHwrites Vulkan SPIR-V shaders throughglslc; intermediate files are placed undertarget/lane-preview.lane preview SOURCEopens the native Vulkan previewer. It usesglslc, FIFO presentation, and a conservative frame cap.lane listlists builtin objects, type aliases, and categories.lane list NAMEshows one builtin object's type and support body.lane list 2dandlane list 3dlist only 2D or 3D primitives.lane list alllists every builtin item on one line, including primitive constructors, GLSL functions, type aliases, object operators, and algebraic categories. Repeated scalar/vector overload families are compacted withRn, square matrix families useMat{n}, rectangular matrix families useMat{n}x{m}, and algebraic helper operations such as component-wise matrix multiplication are omitted from this broad list.lane -pc SHELLprints completion code forbash,zsh, orfish.lane helpis treated as an input path. Uselane -horlane --help.- CLI failures are printed on stderr with an error type prefix such as
lane::Error:orstd::io::Error:. In an interactive terminal, the whole diagnostic is colored red. - Internally, Lane now has explicit preprocessing and postprocessing passes.
The preprocessor canonicalizes Lane surface syntax before typechecking; the
typed postprocessor lowers toward GLSL-oriented core syntax. Rust-defined
objects are tracked as either core syntax/backend machinery or candidates to
move into
std.
The LSP server provides diagnostics by compiling the whole document after open,
change, and save events. Diagnostics resolve #import paths relative to the
open file, so local modules work the same way in the editor and the CLI. The
server also provides formatting, full-document semantic tokens, plus basic
completion, hover, signature help, document links for resolved #import
module paths, and document symbols for Lane keywords, built-in modules,
primitive constructors, type aliases, categories, built-in functions, and
top-level Lane declarations.
The semantic formatter trims trailing whitespace, collapses repeated blank
lines, and normalizes spaced ASCII type products such as R x R to R × R.
The semantic token legend uses Lane-specific
directive, functor, and category token kinds, while mapping imported
modules to namespace, raw GLSL bodies to string, type parameters such as
{n} to typeParameter, and names declared with provided to normal variable
declarations. Standalone type completions stay concrete (for example, R,
R2, R3) and avoid generic placeholders such as R{n} that are not valid as
direct provided type declarations. The REPL is part of the CLI crate and
reuses the lane-lsp completion catalogue, formatting API, and submitted-error
handling.
Neovim built-in LSP example:
vim.filetype.add({ extension = { lane = "lane" } })
vim.lsp.config("lane_lsp", {
cmd = { "cargo", "run", "-p", "lane-lsp" },
filetypes = { "lane" },
root_markers = { "Cargo.toml", ".git" },
})
vim.lsp.enable("lane_lsp")When using the bundled tree-sitter-lane Neovim plugin, require("lane").setup()
registers the filetype, tree-sitter parser/query, and LSP config. Pass
{ lsp = false } to disable the LSP hookup, or pass { lsp = { cmd = { ... } } }
to override the server command.
- Semantic model roadmap describes the current compiler model and the planned object, dependency, GLSL, and generic specialization architecture.
- LSP roadmap describes the current language-server shape and planned editor-facing improvements.
Lane source is line-oriented. Each non-empty, non-comment line is one
declaration. const emits a value, function, or object even if nothing later
references it. For objects, const exports object helper functions.
const Object output is emitted under its Lane name as sdf_output and
grad_sdf_output; output is not a special scene entrypoint.
provided R time
Func(R, R) pulse = pow2 @ sin
Object shape = Ball3D(r=1 + pulse(time))
const Object output = shape
Line comments start with //:
provided R time // animation clock
// final scene
const Object output = Ball3D(r=1 + time)
Directives are line-oriented declarations that start with #. They must appear
before all non-directive declarations.
#2D switches the ambient SDF space from 3D to 2D. It must appear before other
declarations. Under #2D, unqualified Object means Object2D, 3D primitives
and 3D object operators are rejected, and generated entry points use vec2.
#2D
R2 offset = [1, 2]
Object shape = Box2D(a=2, b=1) + offset
const Object output = shape
Other #... lines are not language directives; the compiler treats unknown
directives as errors.
#prec sets the finite-difference epsilon used by generated SDF gradients and
the differential operators provided by std. The default is 0.01. The value
must be a positive float literal.
#import std
#prec 0.002
provided Hom(R3, R) density
provided R3 p
Hom(R3, R3) normal_field = gradient(density)
R3 normal = normal_field(p)
Func(R, R) slope = grad(sin)
const Object output = Ball3D(r=slope(0) + density(normal))
Differential operators do not take per-call epsilon arguments; use #prec when
a program needs a different precision.
Imports a Lane module from the local modules/ directory or the installed Lane
module directory. Imported modules must start with #module, may define Lane
helpers and raw GLSL functions, and cannot contain provided declarations. The
#module directive is only valid in files loaded through #import; scene or
entrypoint files must omit it. The shipped modules include std, complex,
quat, and raytracing. std imports the algebra-focused complex and
quat modules.
#import std
R z = projection_1([3, 4])
The raytracing module provides the Ray, Hit, Material, Camera,
RaytraceConfig, and RaycolorConfig product types plus helpers for preview
shaders. raycolor_from_hit_with contains the raw GLSL reflection loop and is
generic over the hit shading callbacks. Material lookup and material shape stay
in Lane: compose a hit projection with any point-to-material function and
material accessors. The default Material type ships with material_color,
material_emission, and material_reflectiveness accessors. shade composes a
screen-coordinate ray function with a ray-color function; its ray color input is
generic, with the preview R4 output context deducing the concrete color type.
#import raytracing
const Hom(R2, Ray) rays = camera_ray(camera)
const Hom(Hit, R3) color_at = material_color @ material @ hit_position
const Hom(Hit, R3) emission_at = material_emission @ material @ hit_position
const Hom(Hit, R) reflectiveness_at = material_reflectiveness @ material @ hit_position
const Hom(Ray, R3) colors = raycolor_from_hit_with(default_raycolor_config, ambientColor, hit, color_at, emission_at, reflectiveness_at)
const Hom(R2, R4) pixels = shade(rays, colors)
const Hom(*, *) main = fragment_main(pixels)
Declares an external GLSL value, such as a uniform or global constant. Generated SDF helpers and scene entrypoints reference provided values by name; they do not thread provided values through helper parameters.
provided R time
provided R3 center
provided measure: R3 -> R
provided density, shadow: R3 -> R
const Object output = Ball3D(r=1 + time) + center
Function-valued provided inputs can use either provided Hom(A, B) name or the
equivalent morphism form provided name: A -> B. Both forms accept comma-list
names, such as provided Hom(R, R) f, g or provided f, g: R -> R.
Long expressions can continue onto the next physical line when each continuation line starts with whitespace. Tabs are preferred for continuation indentation.
R radius = 1
+ time
const Object output = Ball3D(r=radius)
Declares an external nominal type. Lane knows its category, but the host GLSL environment must provide the representation and operations.
provided Grp G, K
provided G a
provided G b
provided Hom(G, R) measure
R radius = measure(a * b)
const Object output = Ball3D(r=radius)
This emits calls through the compiler-owned operator overload names such as
__mult(a, b). For neutral literals, Lane expects globals such as
__zero_G, __one_G, and __e_G when those operations are needed.
Constructs or promotes a nominal category type from an existing set-like type.
The category prefix is a promise that the required operations are defined for
the new type. Define those operations by overloading operator references such
as &+, &*, and &~, and define neutral slots with typed 0, 1, or e
bindings.
provided Set X
provided X zeroX
provided End(X) negX
provided Hom(X x X, X) addX
Ab X = X
X 0 = zeroX
Hom(X x X, X) &+ = (left, right) |-> addX(left, right)
Hom(X x X, X) &- = (left, right) |-> addX(left, negX(right))
After these declarations, normal expressions such as a + b, a - b, and
0 in an expected X context use __add, __sub, and __zero_X internally.
When TypeName differs from BaseType, Lane emits a one-field wrapper. If the
names are the same, as in Ab X = X, Lane treats the declaration as a
promotion and keeps the host representation unchanged.
Constructs a nominal product type and emits a GLSL struct. Every component must
satisfy the declared category or a subcategory. DivRing products are rejected.
For Grp products, additive abelian components can participate as group
components by interpreting e as 0, multiplication as addition, and inverse
as negation.
Grp G<m, n> = Isom3 x Isom2
Grp Motion = Isom3 x R3
provided G a
provided G b
G product = a * b
const Object output = Ball3D(r=1)
Emitted support includes:
struct G {
Isom3 m;
Isom2 n;
};
G __mult(G a, G b) {
return G(mult_Isom3(a.m, b.m), mult_Isom2(a.n, b.n));
}DivRing names are optional:
Ab Pair = R2 x R3
Pair p = Pair([1, 2], 0)
Default field names are x0, x1, ... for every product arity. For product
types without explicit <...> field names, the legacy positional aliases
x, y, z, and w still refer to x0, x1, x2, and x3 when present.
Without const, product operations are emitted only when used. With const,
all operations for the category are emitted:
const Grp G = Isom3 x Isom2
Declares a typed value, function, or object binding. The right hand side must match the annotated type.
R radius = 1.5
R3 offset = [1, 0, 0]
Object ball = Ball3D(r=radius) + offset
Func(R, R) wobble = pow2 @ sin
Declares a local binding with an inferred type. Inference is accepted only when the expression has one clear type.
radius = 1 + 2
R3 offset = [1, 0, 0]
shape = Ball3D(r=radius) + offset
const Object output = shape
Exports a reusable SDF helper named sdf_name and a gradient helper named
grad_sdf_name. Later uses call the helper instead of inlining the object.
provided R radius
R3 offset = [1, 0, 0]
construct Object shell = Ball3D(r=radius) + offset
const Object output = shell
Object bindings also expose function getters:
#import std
Object shell = Ball3D(r=2)
R d = shell.sdf([0, 0, 0])
R3 normal = shell.grad([0, 0, 0])
Hom(R3, R3) finite_diff_normal_field = gradient(shell.sdf)
R3 finite_diff_normal = finite_diff_normal_field([0, 0, 0])
const Object output = Ball3D(r=d + length(normal + finite_diff_normal))
obj.sdf has type Hom(R3, R) for 3D objects and Hom(R2, R) for 2D
objects. obj.grad returns the matching ambient vector type. Using either
getter emits the same helper functions as construct, even for a plain
Object binding. In a generated value/function expression, bare object getters
lift over the ambient point:
#2D
const rect = Box2D(a=1, b=2)
R4 tint = [.5, .5, .9, 1]
const color = rect.sdf * tint
Here color is emitted as Hom(R2, R4).
Emits an inferred value, function, or object. Object-valued declarations emit
SDF helpers; function-valued declarations emit GLSL helper functions; values
that can be represented as top-level GLSL constants are emitted as const
globals.
const radius = 1.5
const pulse = sin
const shell = Ball3D(r=radius)
Exports a reusable SDF helper like construct. For 3D objects, Lane emits
sdf_name and grad_sdf_name; for 2D objects, Lane emits sdf_name. The name
does not need to be output.
const Object shell = Ball3D(r=2)
const Object scene = Ball3D(r=1)
Lane type names are nominal and case-sensitive.
| Lane type | Alias | GLSL value |
|---|---|---|
Bool |
bool |
|
Float |
R |
float |
Int |
Z |
int |
Vec2 |
R2 |
vec2 |
Vec3 |
R3 |
vec3 |
Vec4 |
R4 |
vec4 |
Mat2, Mat3, Mat4 |
mat2, mat3, mat4 |
|
MatNxM |
N,M in {2,3,4} |
GLSL matMxN |
Isom2 |
2D isometry struct | |
Isom3 |
3D isometry struct | |
Object |
Object3D |
ambient 3D SDF object, or Object2D under #2D |
Object2D |
2D SDF object |
Function type.
Func(R, R) pulse = sin
Func(R, R3) path = [sin, cos, 0]
User-defined function bodies support value inputs such as R, R2, and R3.
Inside a Func(R, T) binding, bare unary functions are implicitly applied to
the generated parameter t:
Func(R, R) wobble = pow2 @ sin + .5
Function type alias used for mathematical maps and external functions.
provided Hom(R3, R) density
provided Hom(R3 x R3, R3) cross
R3 c = cross([1, 0, 0], [0, 1, 0])
If the domain is a product type, function calls use multiple positional
arguments. R x R is an explicit product domain with two real-valued
arguments; R2 is a Euclidean vector type. Lane may use the GLSL isomorphism
between them for selected built-in interop, but it does not collapse one syntax
into the other in user function signatures.
Endomorphism type. Equivalent to Hom(T, T).
provided End(R) loop
R y = loop(0.5)
Array type. Array values are fixed-size at construction time, and indexing uses integer indices.
Array(R) weights = Array(1, 2, 3)
R first = weights[0]
R count = size(weights)
Use concat(left, right) to concatenate arrays:
Array(R) a = Array(1, 2)
Array(R) b = Array(3)
Array(R) c = concat(a, b)
T × U and spaced ASCII T x U create anonymous product domains, mainly for
function types.
provided Hom(R3 × R3, R3) cross_unicode
provided Hom(R3 x R3, R3) cross_ascii
ASCII x is a product separator only when surrounded by whitespace.
A^n is shorthand for an n-fold product A × A × ... × A. Concrete
dimensions lower to structural products, and symbolic dimensions can appear in
generic signatures such as Hom({X}^{n}, {X}).
const Hom(R^3, R) sum3 = v |-> v.x + v.y + v.z
Set Triple = R^{3}
C^5 xs = ((0,1), (0,1), (0,1), 0, 1)
Power products have generic projection and diagonal builtins:
const Hom(R^5, R) pick = p{3}
R radius = p{3}(diag{5}(time))
Parentheses group types.
provided Hom((R3 x R3), R3) cross
Categories classify which algebraic operations are available.
| Category | Meaning | Operations used by Lane |
|---|---|---|
Set |
plain values | no algebraic operations |
Ab |
additive abelian group | 0, +, - |
Mon |
monoid | 1, * |
Grp |
group | e, *, ~ |
Ring |
ring | 0, 1, +, -, * |
DivRing |
division ring | ring operations and / |
RVect |
real vector space | 0, +, -, scalar * and / |
RAlg |
real algebra | ring operations and scalar multiplication |
RDivAlg |
real division algebra | real algebra operations and / |
The category order used by Lane is:
RDivAlg < DivRing
RDivAlg < RAlg
DivRing < Grp < Mon < Set
DivRing < Ring
Ring < Mon
Ring < Ab
RAlg < Ring
RAlg < RVect < Ab
RAlg < Mon
Category names are reserved and cannot be used as value type names.
Numbers are R by default unless an expected type casts them.
R a = 1
R b = .5
R c = 1e-2
Z i = 3
Generated GLSL writes float literals with an f suffix in normal compiler
output, for example 1.0f.
Use previously declared values, functions, objects, or provided inputs.
provided R radius
provided R x, y, z
Object ball = Ball3D(r=radius)
const Object output = ball
In expected-type contexts, Lane casts neutral literals:
R3 origin = 0
Mat3 identity_matrix = e
Isom3 identity_motion = Isom3(e, 0)
0casts to the additive neutral element.1casts to the multiplicative neutral element.ecasts to group or square-matrix identity elements.- Matrix identities can also be written explicitly as
I{n}oreye{n}.
When overloads conflict, Lane prefers the uncast numeric type if that resolves the call; otherwise an explicit expected type may be required.
Bool values cast to Z or R when a numeric type is expected. true becomes
1 and false becomes 0; this applies to literals, variables, and function
results.
provided Bool enabled
R weight = enabled
Z count = enabled
Parentheses group expressions and are used for tuple-shaped function products.
Explicit product domains such as R x R are called with multiple positional
arguments. Brackets construct vectors, complex numbers, quaternions, and
matrices when a vector or matrix type is expected.
R2 uv = [0.5, 1]
C z = [1, 0]
R3 p = [1, 2, 3]
H q = [1, 0, 0, 0]
Mat3 m = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
Matrix names use row-by-column shape. GLSL matrix constructors are emitted in the correct column-major form.
Array values use Array(...). All elements must have the same type. Brackets
are reserved for vector and matrix literals.
Array(R3) points = Array([0, 0, 0], [1, 0, 0])
R3 first = points[0]
Function calls use parentheses.
R y = sin(time)
R z = pow2(y)
R3 c = cross([1, 0, 0], [0, 1, 0])
Operators can be referenced as binary functions with &. Operator references
use the same type deduction and emission as infix syntax. Their natural domain
is an explicit two-argument product such as R x R.
R sum = &+(x, y)
Bool ordered = &<(x, y)
const Hom(R x R, R) wave = sin @ &+
Constructor calls use the same syntax:
Isom3 g = Isom3(e, [1, 2, 3])
Pair p = Pair([1, 2], 0)
Primitive constructors accept named arguments.
Ball3D(r=1)
Box3D(a=1, b=2, c=3)
Triangle2D(p0=[0, 0], p1=[1, 0], p2=[0, 1])
Primitive constructors also accept positional arguments in field order.
Ball3D(1)
Box3D(1, 2, 3)
Segment3D([0, 0, 0], [1, 0, 0])
Named and positional arguments cannot be mixed in one call.
Numeric negation.
R x = -1
R3 p = [-1, -2, -3]
Multiplicative or group inverse. This is the inverse operation for Grp
values; use it for plain groups where / is intentionally not available.
provided Grp G
provided G a
G inv_a = ~a
G identity = ~e
Addition and subtraction for supported additive types. On objects, + with a
vector translates the object.
R a = 1 + 2
Bool toggled = true + false
R3 p = [1, 0, 0] - [0, 1, 0]
Object moved = Ball3D(r=1) + [2, 0, 0]
Multiplication, scalar scaling, group composition, function/object action, or matrix action depending on operand types.
R area = 2 * 3
Bool both = true * false
R3 p = 2 * [1, 0, 0]
Isom3 composed = a * b
R3 moved = composed * [1, 0, 0]
Object rotated = rot([0, 0, 1], 0) * Ball3D(r=1)
Division for fields and scalar division for vector spaces. It is not accepted
for plain Grp values.
C z = [1, 2]
C normalized = z / z
R3 half = [1, 2, 3] / 2
== and != compare Bool, R, and Z values. <, <=, >, and >=
compare R and Z values. All comparison operators return Bool.
Bool same = time == 0
Bool inside = 0 <= time
Bool ordered = count < 4
if(cond) x else y selects between two values of the same type and emits a GLSL
conditional expression. cond must be Bool. The shorthand if(cond) x uses
zero for the else branch when 0 is valid for the type of x.
R clipped = if(inside) distance else 0
R masked = if(inside) distance
Hom(R2, R4) color = if(shape.sdf > 0) foreground else background
f @ g means f(g(t)) for unary functions.
Func(R, R) wave = pow2 @ sin
R y = wave(time)
Functions with matching domains support pointwise arithmetic when their codomain supports the corresponding operation.
Hom(R2, R) h = f + g
Pointwise arithmetic and value function calls can mix functions with ordinary values. When a call overload expects a value type and one or more arguments are functions, Lane lifts the call over the shared function domain and treats value arguments as constants over that domain. Function-typed parameters are passed as functions rather than lifted.
Hom(R2, R) clipped = max(shape.sdf, 0)
Hom(R2, R4) color = blend * (shape.sdf > 0)
t |-> expr builds a function by lifting expr over the parameter t. Product
domains can name each component explicitly.
Hom(R, R) shifted = t |-> sin(t + 1)
Hom(R x R, R) diagonal = (x, y) |-> sin(x + y)
The unit type * is used for shader entry functions. Raw GLSL module templates
that instantiate to Hom(*, *) emit as void main() regardless of the Lane
binding name.
Tuples of functions with the same domain form a vector-valued function when the
expected codomain is R2, R3, or R4. For non-scalar codomains, the tuple
preserves the structural product type, such as Hom(X, Y x Z). The domains
must match as written: Hom(R2, R) and Hom(R x R, R) are different
signatures.
Hom(R, R2) circle = [sin, cos]
R2 p = circle(time)
f x g forms a product map for scalar functions, applying the left function to
the first coordinate and the right function to the second. Unannotated bindings
can infer this scalar tensor-product form.
Hom(R2, R2) warp = sin x cos
fold = sin x sin
R2 q = warp([1, 2])
Array indexing uses square brackets and integer indices.
Array(R) xs = Array(1, 2, 3)
R x = xs[1]
Lane pre-registers GLSL math builtins whose signatures fit Lane's current value
types: Bool, R, Z, R2, R3, R4, and generic matrix families such as
Mat{n} and Mat{n}x{m}. Calls emit as direct GLSL calls and do not add
support bodies.
lane list all prints complete scalar/vector families compactly, for example
Hom(Rn, Rn) for functions available on R, R2, R3, and R4, and
transpose as Hom(Mat{n}x{m}, Mat{m}x{n}); square-matrix-only helpers use
Mat{n}.
provided Mat3 frame
R y = sin(time) + cos(time)
R3 n = normalize([1, 2, 3])
R3 reflected = reflect(n, [0, 1, 0])
R3 color = mix(clamp(reflected, 0, 1), [1, 0, 0], 0.25)
Mat3 adjusted = matrixCompMult(frame, inverse(transpose(frame)))
Matrix identities can be written as I{n} or eye{n}. Matrix basis literals
use E{i}{j} with one-based row and column indices, and the expected Mat...
type sets the full matrix size. Unit vector literals use e{N}{n}, with N as
the vector dimension and n as the one-based component index.
Mat3 eye = I{3}
Mat3 also_eye = eye{3}
Mat3 ez = E{1}{3}
Mat12x3 large = E12_3
R3 y_axis = e{3}{2}
Registered GLSL functions include:
- Angle and trigonometry:
radians,degrees,sin,cos,tan,asin,acos,atan,sinh,cosh,tanh,asinh,acosh,atanh. - Exponential:
pow,exp,log,exp2,log2,sqrt,inversesqrt. Besides GLSL-stylepow(Rn, Rn),powhaspow(Z, Mon)for repeated monoid multiplication.#import complexaddspow(C, C)for complex exponentiation. - Common math:
abs,sign,floor,trunc,round,roundEven,ceil,fract,mod,min,max,clamp,mix,step,smoothstep,fma.minandmaxaccept scalar/vector operands in either order. - Geometric functions:
length,distance,dot,cross,normalize,faceforward,reflect,refract. - Matrix functions:
matrixCompMult,transpose,determinant,inverse. - Fragment derivative functions:
dFdx,dFdy,fwidth. - Complex overloads from
#import complex:inv,exp,log,sqrt,sin,cos,tan,sinh,cosh,tanhonC. - Quaternion overloads from
#import quat: inverse and multiplication onH. - Bool helpers:
not,and,or,xor.
Sampler, image, atomic, packing, and out-parameter GLSL builtins are not registered yet because Lane does not have the corresponding value types or parameter passing forms.
pow2 remains available as a Lane helper:
R squared = pow2(y)
Complex overloads are available for functions such as exp, log, pow, and
sin on C inputs after #import complex. Their GLSL overloads are emitted
only when used.
#import complex
C seed = [1, 0]
C z = exp(seed)
Monoid powers take the integer exponent first:
Mon Pair = R x Z
provided Pair p
const Pair cubed = pow(3, p)
Value-level rot(axis, anchor, angle) constructs an Isom3 isometry.
R3 axis = [0, 0, 1]
Isom3 r = rot(axis, 0, time)
R3 p = r * [1, 0, 0]
const Object output = Ball3D(r=1) + p
Object-level rot(...) rotates an object by applying the inverse transform to
the sampled point. Defaults are accepted:
const Object output = rot([0, 1, 0], [1, 0, 0], 0.5)(Ball3D(r=1))
const Object output = rot(0.5)(Ball3D(r=1)) // axis=(0,0,1), anchor=0
const Object output = rot()(Ball3D(r=1)) // angle=0
Value-level rot2D(anchor, angle) constructs an Isom2 isometry.
#2D
Isom2 r = rot2D([0, 0], time)
const Object output = r * Box2D(a=1, b=.5)
Object-level rot2D(...) rotates 2D objects:
const Object output = rot2D([1, 0], 0.5)(Box2D(a=1, b=.5))
const Object output = rot2D(0.5)(Box2D(a=1, b=.5)) // anchor=0
const Object output = rot2D()(Box2D(a=1, b=.5)) // angle=0
Differential operators live in the standard library. Import std before using
them.
#import std
Func(R, R) slope = derivative(sin)
Func(R, R) slope_default = grad(sin)
For scalar fields, derivative, gradient, and grad all produce the
finite-difference gradient in the domain dimension.
#import std
provided Hom(R3, R) density
provided R3 p
Hom(R3, R3) density_gradient = gradient(density)
R3 n = density_gradient(p)
Partial derivatives are named by axis and are available where the input dimension includes that axis:
#import std
provided Hom(R3, R) density
provided R3 p
Hom(R3, R) density_x = dfdx(density)
Hom(R3, R) density_y = dfdy(density)
Hom(R3, R) density_z = dfdz(density)
R along_x = density_x(p)
R along_y = density_y(p)
R along_z = density_z(p)
For vector-valued functions, derivative returns the corresponding Jacobian
matrix. Divergence is available for same-dimensional vector fields:
#import std
provided Hom(R2, R3) field
provided Hom(R3, R3) flow
provided R2 uv
provided R3 p
Hom(R2, Mat2x3) field_derivative = derivative(field)
Hom(R3, R) flow_divergence = divergence(flow)
Mat2x3 jacobian = field_derivative(uv)
R outflow = flow_divergence(p)
Objects denote SDFs. Primitive constructors, object operators, and ambient
actions produce Object or Object2D values.
Object a = Ball3D(r=2)
R3 offset = [2, 0, 0]
Object b = Box3D(1, .5, .25) + offset
Object c = smoothUnion(.2)(a, b)
const Object output = c
Object property operators attach metadata while preserving the object's SDF. The current compiler type-checks the payloads and keeps the metadata operators out of generated distance code; later passes can use the same surface for material lookup, finite bounds, Lipschitz certificates, and distance-aware material blending.
const RVect Paint<color, weight> = R3 x R
const Set Bounds3D<min, max, finite> = R3 x R3 x Bool
const Paint paint = Paint((1, 0, 0), .8)
const Bounds3D bounds = Bounds3D((-1, -1, -1), (1, 1, 1), true)
Object ball =
withBlend(.5)(
withLipschitz(1)(
withBounds(bounds)(
withMaterial(paint)(Ball3D(r=1)))))
withMaterial accepts any Lane value, so material data can be a dedicated
Material, a color vector, or another convex-space value. withBounds accepts
any bounds record, withLipschitz stores a scalar placeholder certificate, and
withBlend accepts a blend descriptor for future material interpolation.
Ball3D(r=1)
Box3D(a=1, b=2, c=3)
Halfspace3D(n=(0, 1, 0), h=0)
Line3D(x0=[0, 0, 0], dir=[1, 0, 0])
Plane3D(n=[0, 1, 0], origin=[0, 2, 0])
Quad3D(p1=[0, 0, 0], p2=[1, 0, 0], p3=[1, 1, 0], p4=[0, 1, 0])
Segment3D(a=[0, 0, 0], b=[1, 0, 0])
Segment3D(2) // centered length constructor
Simplex3D(p0=[0, 0, 0], p1=[1, 0, 0], p2=[0, 1, 0], p3=[0, 0, 1])
Torus3D(major=3, minor=.5)
Triangle3D(p1=[0, 0, 0], p2=[1, 0, 0], p3=[0, 1, 0])
Ball2D(r=1)
Box2D(a=1, b=.5)
Point2D(at=(3, 4))
Polygon2D(points=((0, 0), (2, 0), (2, 1), (0, 1)))
Quad2D(p1=[0, 0], p2=[1, 0], p3=[1, 1], p4=[0, 1])
Segment2D(a=[0, 0], b=[1, 0])
Segment2D(length=2) // centered length constructor
Triangle2D(p0=[0, 0], p1=[1, 0], p2=[0, 1])
Associative operators accept two or more objects:
const Object output = union(Ball3D(r=1), Box3D(1, 1, 1), Torus3D(2, .2))
const Object output = intersect(Ball3D(r=2), Box3D(1, 1, 1))
R3 offset = [1, 0, 0]
const Object output = xor(Ball3D(r=1), Ball3D(r=1) + offset)
Binary difference accepts exactly two objects:
const Object output = diff(Box3D(2, 2, 2), Ball3D(r=1))
Smooth operators are curried by smoothing radius k.
const Object output = smoothUnion(.2)(Ball3D(r=1), Box3D(1, 1, 1))
const Object output = smoothIntersect(.2)(Ball3D(r=2), Box3D(1, 1, 1))
const Object output = smoothDiff(.2)(Box3D(2, 2, 2), Ball3D(r=1))
R3 offset = [1, 0, 0]
const Object output = smoothXor(.2)(Ball3D(r=1), Ball3D(r=1) + offset)
Lifts an Object2D profile into a 3D surface of revolution.
Object2D profile = Segment2D(a=[0, -1], b=[0, 1])
const Object output = revolution(1.5)(profile)
Extrudes a 2D profile along the z axis.
const Object output = extrude(.25)(Box2D(a=1, b=.5))
Translate objects with vector addition:
R3 offset = [1, 2, 3]
const Object output = Ball3D(r=1) + offset
Apply orthogonal matrix actions or isometry actions with *:
provided Mat3 R
const Object output = R * Ball3D(r=1)
Isom3 g = Isom3(e, [1, 2, 3])
const Object output = g * Ball3D(r=1)
Under #2D, Isom2 * Object2D is accepted:
#2D
Isom2 g = Isom2(e, [1, 2])
const Object output = g * Box2D(a=2, b=1)
Every compilation emits:
- support structs and helper functions for used primitives, operators, value functions, built-in algebraic types, and constructed product types;
- user value functions emitted with their Lane name;
- generated object helpers for
constructorconst Objectbindings:sdf_nameandgrad_sdf_namefor 3D objects, andsdf_namefor 2D objects.
Scene-invariant value bindings are emitted as global const values when
possible. Generated local names are renamed if they would collide with user
names such as p, eps, dx, dy, or dz.
provided R time
provided Hom(R3, R) density
Grp Motion<left, right> = Isom3 x Isom3
R3 axis = [0, 0, 1]
Isom3 spin = rot(axis, 0, time)
Motion both = Motion(spin, Isom3(e, [2, 0, 0])) * Motion(Isom3(e, 0), spin)
construct Object ball = Ball3D(r=1 + sin(time))
Object box = Isom3(e, [2, 0, 0]) * Box3D(a=1, b=.5, c=.5)
Object scene = smoothUnion(.2)(spin * ball, box)
const Object output = scene
Use the CLI for authoritative registered GLSL bodies:
lane list 3d
lane list revolution
lane list Isom3The repository includes compile-valid feature samples:
examples/all_features.laneuses the 3D language surface: provided inputs, provided category types, constructed product types, typed and inferred bindings,construct,const Object, arrays, product domains, categories, neutral casts, differential builtins, value-level rotations, object-level rotations, primitive constructors, object operators, revolution, extrusion, and ambient actions.examples/all_features_2d.lanecovers the#2Dambient mode and 2D object actions.
Compile them directly:
cargo run -- examples/all_features.lane
cargo run -- examples/all_features_2d.lane