Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

The µcad programming language is a purely declarative so-called markup programming language, which means that a µcad program can be evaluated (like a mathematical equation) resulting in a graphical output.

Purpose of this document

This document is meant as an explaining reference of all features of µcad. So chapter-wise this book will explain them around some example code. This example code is automatically tested and gives us a bit of a guarantee, that those features we explain here, work like we say.

Contribute

You might directly contribute your changes or new texts to this book via our git repository or if you are not so familiar with git, just leave an issue at codeberg.org so we can implement your ideas.

❤️ Donate

If you like this project, you can help us spend more time on it by donating:

Preface

In the 2010s, 3D printing suddenly became accessible to almost everyone — whether in maker spaces or even at home. People began building their own DIY printers and writing software to control them. Naturally, these developers needed a platform to design 3D models that could be printed.

Around that time, someone came up with an idea: a programming language made specifically for people who prefer working with a keyboard rather than a mouse or graphics tablet. That language is called OpenSCAD. It turned out to be a great success, inspiring countless impressive projects.

We loved it too and created many “thingies” with it. However, as experienced software engineers, we also had a few points of critique. While OpenSCAD is easy to learn and has a syntax reminiscent of C, we felt the language could be improved in several ways:

  • more specialization for creating graphics,
  • better support for modular programming,
  • strict typing and unit handling,
  • a syntax closer to Rust than to C,
  • a solid library system,
  • plugin support for other programming languages,
  • and a more powerful visualization concept.

Over the past few years, as we became more familiar with Rust, we started microcad as a fun side project. In 2025, we were fortunate to receive funding to develop a visualization plugin.

After more than a year of work, we’ve developed and partially implemented several concepts. There’s still plenty to do and many more ideas to explore — but we believe we’ve now reached a point where it’s time to share our work with others.

Automatic Tested Code Examples

Within this document all code examples are automatically tested.

Test result banners

Each code example has a banner nearby which indicates the current testing status of the code. If you click one of these banners you will jump directly into a specific test report.

The following banners indicate that the code is tested ok:

BannerMeaning
okOk
okOk (with warnings)
fail_okFails intentionally

The following banners occur if a test is still marked as todo but is ok already. This can be corrected by changing the documentation.

BannerMeaning
not_todoMarked as todo but is ok
not_todo_failMarked as todo but fails intentionally

If you see one of the following banners, we did something wrong. Either the example may be wrong or the µcad interpreter might have a bug.

BannerMeaning
failFails with errors
fail_wrongFails with wrong errors or warnings
ok_failIs ok but was meant to fail
ok_wrongIs ok but with wrong warnings
parse_failFails early while parsing

The following banners occur if tests are marked as todo and so are not running successful.

BannerMeaning
todoWork in progress
todo_failWork in progress (should fail)
todo_wrongWork in progress (fails with wrong warnings)

Test list

See this section for a list of tests within this document.

Program Structure

A µcad program primarily consists of the following core elements:

This section just explains the core elements but each may contain various statements.

Source Files

Source files are files that contain µcad source code. Such files must either have the extension .µcad, .mcad or .ucad.

A source file can include the following types of statements which we will all discuss within this book:

StatementPurposeExample
expressioncalculate valuesx * 5;
assignmentstore valuesy = x;
const assignmentnaming constantsconst Y = 1;
pub assignmentexporting constantspub Y = 1;
functionseparate calculationsfn f() { }
workbenchbuild or transform 2D sketches and 3D partspart P() { }
modulemodularization of complex codemod m { }
ifprocess conditionsif x > 1 { }
useuse symbols from other modulesuse m;
calluse functions and workbenchesf();
commentfor documentation// comment

In its simplest form, a µcad program consists of a single file containing one or more of the above statements.

A source file can serve as both a module and a workbench. You can use it to provide structure (for example, by organizing submodules) or as a kind of workbench where you add statements to generate outputs—such as a 2D graphic or a 3D model.

The workbench section of the file is only evaluated if it is in the main file (the one that microcad was called with).

The following examples illustrate this: a circle and a sphere are created, each with a radius of one centimeter.

test

// simply draw a circle
std::geo2d::Circle(radius = 1cm);

test

// simply draw a sphere
std::geo3d::Sphere(radius = 1cm);

Statements within a source file can represent either a 2D or a 3D model — but not both at the same time. Mixing 2D and 3D statements in the same file will result in an error:

test

std::geo2d::Circle(radius = 1cm);
std::geo3d::Sphere(radius = 1cm);  // error: can't mix 2D and 3D

However, µcad programs can also be split across multiple files. To include other files, the mod statement is used…

Modules

A module is a way to organize and group code into logical units. Module are declared with the mod keyword. Modules help to manage scope and define namespaces in your code. Each module has a unique name that must be written as lower_snake_case.

In µcad, there are two kinds of modules:

  • inline modules: Inline modules defined by a name and a body inside a file: mod my_module { ... }.
  • file modules: File modules have their definition in a separate file: mod my_file;.

The visibility of modules of modules can be controlled via the pub qualifier. If a module is qualified as pub, it becomes visible to external.

Example

The following example shows two nested inline modules carrying a public constant and a public function which then are used from outside the modules:

test

// define private module
mod my {
    // define public module
    pub mod math {
        // define PI as a public constant value
        pub const PI = 3.14159;

        // define some public calculation function
        pub fn abs(x: Scalar) -> Scalar {
            if x < 0 { return -x; } else { return x; }
        }
    }
}

// use the function and the constant
x = my::math::abs(-1.0) * my::math::PI;

Inline modules

Inline modules are modules are defined with the mod keyword, followed by a name and a body {...} as the following example shows:

test

mod my_module {
    // e.g. define a constant
    pub VALUE = 1;
}

The following statements can be used within an inline module:

StatementPurposeExample
const assignmentnaming constantsconst Y = 1;
pub assignmentexporting constantspub Y = 1;
functionseparate calculationsfn f() { }
workbenchbuild or transform 2D sketches and 3D partspart P() { }
modulemodularization of complex codemod m { }
useuse elements from other modulesuse m;
commentfor documentation//! comment

File Modules

File modules are modules that include µcad source files.

For example, if you put another file second.µcad next to your main file, you can easily import this second file via its name second:

test

mod second;
second::f(); 

test

// file: second.µcad
pub fn f() {}

By writing mod second; in the first source file, microcad searches for either a file called second.µcad or second/mod.µcad and loads its public symbols into a module.

Please note that according to µcad code convention that says that module name have to be lower snake code, file names have to be written lower snake case, too.

Hint: Because file modules are source files, they may contain statements that are not allowed in inline modules. These statements (such as calls, expressions, or value assignments) will not be processed when including a file module.

Use Statements

The use statement imports symbols from one module into another’s local namespace. This allows symbols to be referenced by their short names rather than their fully qualified names, reducing boilerplate and improving code readability.

The following example which uses two parts of std::geo3d shows the problem with long names:

test

std::geo3d::Sphere(radius = 40mm);
std::geo3d::Cube(size = 40mm);

There are several ways to solve this with use.

use Statement

A use statement creates a local alias for a specific symbol. While the initial declaration takes space, it significantly shortens subsequent calls.

Looking at the example below, using use does not seem any shorter, but if we would use Sphere and Cube repeatedly, this would shorten things quite a lot:

test

use std::geo3d::Sphere;
use std::geo3d::Cube;

Sphere(radius = 4mm);
Cube(size = 40mm);

Alternatively, you may also use an entire module geo3d and to get rid of the std:: prefix part within the names:

test

use std::geo3d;

geo3d::Sphere(radius = 40mm); // Drops the `std::` prefix.

use ... as Statement

Internally, every use statement defines an alias with an identifier (e.g. Sphere) and a target symbol it points to (e.g. std::geo3d::Sphere).

To prevent name collisions or to improve clarity, you can provide a custom identifier using the as keyword:

test

use std::geo3d::Sphere as Ball;

Ball(radius = 4mm);

Of course you can use use as with a whole module:

test

use std::geo3d as space;

space::Sphere(radius = 4mm);

use * Statement

The shortest way to use many symbols from one module is to use * as target. The following example defines aliases for all symbols of std::geo3d.

test

use std::geo3d::*;

Sphere(radius = 4mm);
Cube(size = 40mm);
Torus(major_radius = 40mm, minor_radius = 20mm);

The disadvantage of using * is that it increases the risk of name conflicts between your code and the aliased symbols, some of which you might not even use.

Public Use Statement

The pub use statement does not only make the target symbol visible within the module in which it is defined, but from the outside, too. See the visibility section for information.

test

mod my {
    pub use std::geo3d::Sphere;
    pub use std::geo3d::Cube;
}

my::Sphere(radius = 4mm);
my::Cube(size = 40mm);

Functions

Functions provide a way to encapsulate frequently used code into sub-routines. These sub-routines can then be called to execute their code with a specific set of parameters.

The function definition starts with the keyword fn, followed by an identifier, a parameter list, and a function body.

test

// define function print_error with text as parameter of type String
fn print_error( text: String ) {
    // code body
    std::print("ERROR: {text}");
}

print_error("first");
print_error("second");
Output
ERROR: first
ERROR: second

Default Parameters

Parameters can include default values. Default values are specified without an explicit type annotation, but their type is inferred from the unit of the provided value.

test

fn f(x: Scalar, y=1mm) -> Length {
    x * y
}

std::debug::assert_eq([ f(2), 2mm ]);
std::debug::assert_eq([ f(2, 2mm), 4mm ]);

Functions may be declared within source files, modules or workbenches.

Function Results

Of course, functions can also return a value. To do so, you need to define the return type (using ->) as shown in the following example:

test

fn square( x: Scalar ) -> Scalar {
    x * x
}

std::debug::assert_eq([ square(8.0), 64.0 ]);

You may also use if to decide between different results.

test

fn pow( x: Scalar, n: Integer ) -> Scalar {
    if n > 1 {
        x * pow(x, n-1)
    } else if n < 1 {
        1.0 / x / pow(x, -n)
    } else {
        1.0
    }
}

std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);

Of course returning a value twice is not allowed.

test

fn pow( x: Scalar, n: Integer ) -> Scalar {
    if n > 1 {
        x * pow(x, n-1)
    } else if n < 1 {
        1.0 / x / pow(x, -n)
    }
    1.0 // error: without else this line would return a second value
}

std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);

Early Return

It is also possible to implement an early return pattern with the return statement.

test

fn pow( x: Scalar, n: Integer ) -> Scalar {
    if n > 1 {
        return x * pow(x, n-1);
    }
    if n < 1 {
        return 1.0 / x / pow(x, -n);
    }
    1.0
}

std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);

Module Functions

A module can contain functions that are accessible within the module. By declaring a function as public using the keyword pub, it becomes available for use outside the module.

test

// module math
mod math {
    // pow cannot be called from outside math
    fn pow( x: Scalar, n: Integer ) -> Scalar {
        if n == 1 {
            x   // return x
        } else {
            x * pow(x, n-1) // return recursive product
        }
    }

    // square is callable from outside math
    pub fn square(x: Scalar) -> Scalar {
        // call internal pow
        pow(x, 2)
    }
}

// call square in math
math::square(2.0);
math::pow(2.0, 5);  // error: pow is private

Workbench Functions

A workbench can contain functions. These functions can be used within the workbench, but not from outside it.

Here is an example which generates a punched disk of a given radius using a function inner():

test

sketch PunchedDisk(radius: Length) {
    use std::geo2d::Circle;

    // function to calculate inner from radius
    fn inner() { radius/2 }

    // generate donut (and call inner)
    Circle(radius) - Circle(radius = inner());
}

PunchedDisk(1cm);

Restrictions

There are some restrictions for workbench functions:

No public workbench functions

Trying to make them public with the keyword pub will result into an error:

test

sketch PunchedDisk(radius: Length) {
    use std::geo2d::Circle;

    pub fn inner() { radius/2 }   // error: cant use pub fn inside workbench

    Circle(radius) - Circle(radius = inner());
}

PunchedDisk(1cm);

No prop in workbench functions nor initializers

You cannot create workbench properties in function bodies.

test

sketch PunchedDisk(radius: Length) {
    use std::geo2d::Circle;

    fn inner() {
        prop hole = radius/2;  // error: prop not allowed in function
        hole
    }

    prop hole = radius/2;      // correct prop definition

    Circle(radius) - Circle(radius = inner());
}

PunchedDisk(1cm);

Also the prop keyword is not allowed in initializers. Instead, the properties of the building plan must be set directly, without using the prop keyword.

test

sketch PunchedDisk(radius: Length) {
    use std::geo2d::Circle;

    init(diameter: Length) { 
        prop radius = diameter/2; // error: prop not allowed in init
    }
    
    init(d: Length) { 
        radius = d/2;             // correct way to set radius
    }

    // Accessing property in a function is ok
    fn inner() { radius/2 }

    Circle(radius) - Circle(radius = inner());
}

PunchedDisk(diameter=1cm);
PunchedDisk(d=1cm);

Workbenches

Workbenches are essential language elements in µcad for creating and constructing 2D sketches, 3D parts, or performing operations on them. Every workbench is initialized with a building plan from which it generates 2D or 3D models.

In the following pages, you will first learn about the elements that compose a workbench of any type. Then, we will explore the different types of workbenches.

Workbench Elements

A workbench in general consists of the following elements:

  • A leading keyword: part, sketch, or op,
  • an identifier that names the workbench,
  • a building plan defined by a parameter list following the identifier,
  • optional initialization code, which is placed and executed before any initializer,
  • optional initializers, offering alternative ways to initialize the building plan,
  • optional functions, acting as local subroutines with their own parameters and code bodies,
  • optional properties, accessible from outside and set from the inside,
  • and typically some building code, which runs after all initialization steps and generates the final objects.

The following code demonstrates (on the basis of a sketch) most of these elements which we will discuss in the following pages in detail.

test

// Sketch with a `radius` as building plan.
// Which will automatically become a property.
sketch Wheel(radius: Length) {
    
    // Initialization code...
    const FACTOR = 2;

    // Initializer #1
    init(diameter: Length) {
        // must set `radius`
        radius = diameter / FACTOR;
    }

    // No code in between!

    // Initializer #2
    init(r: Length) {
        // must set `radius`
        radius = r;
    }

    // Function (sub routine)
    fn into_diameter(r: Length) {
        return r * FACTOR;
    }

    // Building code...

    // Set a property which can be seen from outside.
    prop diameter = into_diameter(radius);
    
    // Local variable `r`
    r = radius;
    
    // Create a circle.
    std::geo2d::Circle(r);
}

use std::debug::*;

// Call sketch with diameter.
d = Wheel(diameter = 2cm);
// Check radius property.
assert_eq([d.radius, 1cm]);

// Call sketch with radius.
r = Wheel(radius = 2cm);
// Check diameter property.
assert_eq([r.diameter, 4cm]);

r - d;
Output
output

Building Plan

The building plan is defined by a parameter list that follows the workbench’s identifier. All parameters in that list will automatically become properties of the workbench when it is invoked. These properties can be accessed within the building code, inside functions, or externally.

The following code demonstrates this using a sketch with a single parameter in the building plan, called radius, of type Length:

test

// sketch with a radius as building plan
sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;

    // access property radius from the building plan
    Circle(radius = radius + thickness) - Circle(radius)
}

// access property radius of a Wheel
w = Wheel(1cm);
// render Wheel
w;

// check if r is 5cm an thickness equals the default (1cm)
std::debug::assert_eq( [w.radius, 1cm] );
std::debug::assert_eq( [w.thickness, 5mm] );
Output
output

Initializers

Initializers are a way to define alternative parameters to create the building plan. An Initializer is defined with the keyword init and a following parameter list. One may define multiple initializers which must have different parameter lists.

However, if an initializer is used, all properties from the building plan must be initialized (except those with default values).

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;

    // initializer with diameter
    init( diameter: Length, thickness = 5mm ) {
        // must set property `radius` from building plan
        radius = diameter / 2;

        // `thickness` (from the building plan) does not need 
        // to be set, because it was automatically set by 
        // parameter of this initializer
    }

    // Now radius and thickness can be used
    Circle(radius = radius + thickness) - Circle(radius)
}
// call with building plan
Wheel(radius=1.5cm, thickness=2mm);
// call with initializer and use default thickness
Wheel(diameter=1.5cm);
Output
output

Restrictions

Building plan must be initialized

If the building plan is not fully initialized by an initializer you will get an error:

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;

    init( thickness: Length ) { } // error: misses to set radius from building plan

    Circle(radius = radius + thickness) - Circle(radius)  // error: radius is missing
}
Wheel(thickness = 1cm);

Building plan properties with default values

Parameters of a workbench’s building plan which have a default value do not need to be set in the initializers.

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;
    
    init(diameter: Length) { 
        radius = diameter / 2;
        // thickness has been set automatically by the default in the building plan 
    }
    
    Circle(radius = radius + thickness) - Circle(radius)
}

Wheel(diameter = 1cm);
Output
output

Building plan cannot be accessed within initializers

You cannot read building plan items from within initializers.

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;
    
    init( diameter: Length, thickness = 5mm ) { 
        _ = radius;            // error: cannot read radius here
        radius = diameter / 2; // instead you need to set it
    }
    
    Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(diameter = 1cm);

Initializers with parameters from building plan

If you use parameter names in an initializer which already are used in the building plan, they will automatically become properties and cannot be set second time.

test

sketch Wheel(radius: Length, thickness: Length) {
    use std::geo2d::Circle;

    init( radius: Length ) {
        // radius property has already been set by building plan
        radius = radius * 2;  // error: it cannot be set a second time
        thickness = 5mm;
    }
    Circle(radius = radius + thickness) - Circle(radius)
}
// Use initializer
Wheel(radius = 1cm);

Types must match when using a name from building plan in initializer parameters.

test

sketch Wheel(radius: Length, thickness: Length) {
    use std::geo2d::Circle;

    init( radius: Scalar, outer: Length ) { // error: radius is already a Length in building plan
        thickness = outer - (radius * 1mm);
    }  

    Circle(radius = radius + thickness) - Circle(radius)
}
// Use initializer
Wheel(radius = 1.0, outer = 1cm);

No code between initializers

It’s not allowed to write any code between initializers.

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;

    init( width: Length, thickness = 5mm ) { radius = width / 2; }
    radius = 1; // error: code between initializers not allowed
    init( height: Length, thickness = 5mm ) { radius = height / 2; }

    Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(radius = 1cm);

Initialization Code

If you use initializers you might place some initialization code on top of the workbench’s body (before the first initializer).

The initialization code is just allowed to define some constants which then can be used in all following code (including code within initializers and functions).

test

sketch Wheel(radius: Length) {
    // init code
    const FACTOR = 2.0;

    // initializer
    init(diameter: Length) { into_radius(diameter) }

    // function
    fn into_radius( diameter: Length ) {
        // use constant FACTOR from init code
        diameter / FACTOR
    }

    // set property diameter and use FACTOR from init code
    prop diameter = radius * FACTOR;
    
    // code body
    std::geo2d::Circle(radius);
}

std::debug::assert_eq([ Wheel(radius = 5cm).radius, 5cm ]);
std::debug::assert_eq([ Wheel(radius = 5cm).diameter, 10cm ]);

If there are no initializers, the initialization code is just part of the building code.

Restrictions

Cannot access building plan in initialization code

test

sketch Wheel(radius: Length, thickness = 5mm) {
    use std::geo2d::Circle;

    const _ = radius * 2;   // error: cannot use radius from building plan

    init( diameter: Length ) { radius = diameter / 2; }
    Circle(radius + thickness) - Circle(radius)
}

Wheel(radius = 1cm);

Building Code

The building code is executed after any initialization. Usually it produces one or many 2D or 3D objects on base of the given building plan.

test

sketch Wheel(radius: Length, thickness = 5mm) {
    // building code starts here
    use std::geo2d::Circle;
    Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(radius = 1cm);

If initializers were defined the building code starts below them.

test

sketch Wheel(radius: Length, thickness = 5mm) {
    // initializer code
    use std::geo2d::Circle;
    // initializer
    init( diameter: Length ) { radius = diameter / 2; }

    // building code starts here
    std::geo2d::Circle(radius);
}
Wheel(radius = 1cm);

Restrictions

Illegal statements within workbenches

It’s not allowed to use the sketch, part, op, return nor mod statements within workbench code:

test

sketch Wheel(radius: Length) {
    sketch A() {}   // error
    part B() {}     // error
    op C() {}       // error
}

test

sketch Wheel(radius: Length) {
    mod m {}        // error
}

test

sketch Wheel(radius: Length) {
    return;         // error
}

Properties

There are two ways to declare Properties:

  • as parameter of the building plan or
  • in an assignment within the build code by using the keyword prop.

In the following example we declare a building plan which consists of a radius which will automatically be a property:

test

// `outer` will automatically become a property because
// it is declared in the building plan:
sketch Wheel(outer: Length) {
    use std::geo2d::Circle;

    // `inner` is declared as property
    prop inner = outer / 2;

    // generate wheel (and use property `outer` and `inner`)
    Circle(radius = outer) - Circle(radius = inner);
}

// evaluate wheel
t = Wheel(1cm);

// extract and display `outer` and `inner` from generated wheel
std::print("outer: {t.outer}");
std::print("inner: {t.inner}");
Output
outer: 10mm
inner: 5mm

If you remove the prop keyword you will fail at accessing inner:

test

sketch Wheel(outer: Length) {
    use std::geo2d::Circle;

    // `inner` is declared as variable and may not be read
    // from outside this workbench
    inner = outer / 2;

    Circle(radius = outer) - Circle(radius = inner);
}

t = Wheel(outer = 1cm);

// you can still extract and display `outer`
std::print("outer: {t.outer}");
// but you cannot access `inner` anymore
std::print("inner: {t.inner}"); // error

Restrictions

No prop within initializer

You may not define a property within an initializer.

test

sketch Wheel(radius: Length, thickness = 5cm) {
    init(radius: Length, inner: Length) {
        thickness = radius - inner;

        prop center = radius - inner;       // error: do not use prop here
    }
    
    prop center = radius - thickness / 2;   // here it's ok
}
center = Wheel(radius = 1cm, inner = 0.5cm).center;

std::debug::assert_eq([ center, 1.75cm ] );
Output
test

No prop within initialization code

Also you may not use prop within initialization code.

test

sketch Wheel(outer: Length) {

    prop max = 100;     // error: do not use prop here

    init(inner: Length) {
        outer = inner * 2;
    }
}
Wheel(inner = 0.5cm);

Workbench Types

Workbenches come in three flavors:

TypeKeywordInput ModelOutput Model
partspartnone3D
sketchessketchnone2D
operationsop2D or 3D2D or 3D

Mostly you may start directly with part or with a sketch which you then operate (with an op) into a part.

Parts

Parts are workbenches that are used to create 3D models. They are named in PascalCase:

test

part MyPart( radius: Length ) {
    use std::geo3d::*;
    Sphere(radius) - Cube(radius)
}

MyPart(1cm);

Like all workbenches parts can have several workbench elements.

Restrictions

Parts cannot generate 2D models

You will get an error if you generate a 2D model with a part:

test

sketch MyPart( radius: Length) {
    use std::geo2d::*;
    Circle(radius) - Rect(radius);  // error: Circle and Rect are 2D
}

MyPart(1cm);

Sketches

Sketches are similar to parts but in two dimensions only. They may be extruded into three-dimensional parts by using operations. Sketches are named in PascalCase:

test

sketch MySketch( radius: Length) {
    use std::geo2d::*;
    Circle(radius) - Rect(size = radius);
}

MySketch(1cm);

The output is a 2D sketch:

test

Restrictions

Sketches cannot generate 3D models

You will get an error if you generate a 3D model with a sketch:

test

sketch MySketch( radius: Length) {
    use std::geo3d::*;
    Sphere(radius) - Cube(size = radius);  // error: Sphere and Cube are 3D
}

MySketch(1cm);

Operations

Note

Do not confuse operations with operators

Operations process 2D or 3D geometries into 2D or 3D geometries. Unlike sketches or parts they are named in snake_case.

Actual operations are workbenches that process input models into output models. So the following nop operation would be a neutral operation which just passes-through the original input model:

test

// define operation nop without parameters
op nop() { @input }

// use operation `nop` on a circle results in the same circle
std::geo2d::Circle(radius = 1cm).nop();
Output
test

@input

@input is a placeholder to tell where the input nodes of the operation shall be inserted.

An operation can have multiple children when they are bunched together in a group. In the following example punshed_disk awaits a group of exactly two children.

test

// define operation which takes multiple items
op punched_disk() { 
    // check number of input models
    if @input.count() == 2 {
        // make hole by subtracting both inputs
        @input.subtract(); 
    } else {
        std::error("punched_disk must get exactly two objects");
    }
}

// use operation punch_disk with two circles
{
    std::geo2d::Circle(radius = 1cm);
    std::geo2d::Circle(radius = 2cm);
}.punched_disk();
Output
test

Like other workbenches operations can have parameters too:

test

// define operation which takes multiple items
op punch_disk(radius: Length) {
    if @input.count() == 1 {
        { 
            @input;
            std::geo2d::Circle(radius);
        }.subtract();
    } else {
        std::error("punch_disk must get one object");
    }
}

// use operation punch_disk on a circle
{
    std::geo2d::Circle(radius = 2cm);
}.punch_disk(radius = 1cm);
Output
test

Expressions

An expression defines a value simply by a literal or by combining multiple other expressions. For example, we can multiply a quantity of 4 millimeters with a factor 5 and assign it to a constant v with the following code:

test

v = 5 * 4.0mm;

std::debug::assert_eq([ v, 20mm ]);

The result of this expression would be 20mm like the test (see assert_eq) proves. In this form, the computed value is not yet used for anything, so the examples above will not produce any visible output or effect.

Expression result types

In the above example v is of type Length because the expression 5 * 4.0mm is the multiplication of a factor (without unit) and a length (in mm).

Scalar & Quantity results

In general units of quantities are calculated according to the operator and the units of the operands in an expression like the following examples show.

test

use std::debug::assert_eq;

// an integer multiplied with another one remains a integer
assert_eq([ 5 * 4, 20 ]);

// a scalar multiplied with another scalar or integer results in a scalar
assert_eq([ 5.5 * 4.5, 24.75 ]);
assert_eq([ 5.1 * 4, 20.4 ]);
assert_eq([ 5 * 4.1, 20.5 ]);

// a scalar multiplied with a length is a length
assert_eq([ 5 * 4mm, 20mm ]);

// two length multiplied with each another is an area
assert_eq([ 5mm * 4mm, 0.2cm² ]);

// dividing an area by a length is a length
assert_eq([ 20mm² / 4mm, 5mm ]);

Boolean results

Boolean operations (e.g. !, >, == or &&) lead to a boolean result.

test

// Using logical operators lead to a boolean result
std::debug::assert_eq([ 5mm > 4mm, true ]);

Even when using them with models:

test

// Using logical operators between models too
std::debug::assert_eq([ std::geo2d::Circle(1cm) == std::geo2d::Circle(10mm), true ]);

Only Boolean expressions (expressions with a boolean result) can be used to define conditions (see if statement).

Literals

Literals are the simplest form of expressions. In this section, we will look at some literals.

test

5;        // Scalar literal
4.0mm;    // Quantity literal
"Hello";  // String literal

There are several types of literals:

NameEncodingDescription
Integer64 bit integersigned integer
Scalar64 bit floatsigned floating point
Boolean1 bit boolboolean
StringUTF-8Text
Quantities64 bit floatsigned floating point (including type)

Integer Literals

Integer literals contain a whole number with a sign (but without a unit). Here are a few examples:

test

50;
1350;
-6

Scalar Literals

Scalar literals contain a floating point number with a sign (but without a unit).

test

1.23;
0.3252;
.4534;
1.;
-1200.0;
12.0E+12;
50%    // = 0.5

Boolean Literals

Booleans contain either the value true or false:

test

true;
false

String Literals

Strings are texts enclosed in quotation marks:

test

"Hello, World!"

Quantity Literals

Quantities are like scalars but with a unit and are widely used in microcad if you wanna draw something.

test

4.0mm;   // 4 millimeters
5l;      // 5 liters
4m²;     // 4 square meters
4m2;     // also 4 square meters
10°;     // 10 degree angle
10deg    // also 10 degree angle

You will find more details about quantities and units in this section.

Operators

Note

Do not confuse operators with operations

There are several operators which can be used to combine expressions with each other:

OperatorType1st Operand12nd OperandResult TypeDescription
!unaryB-sameInversion
-unaryI Q A T-sameNegation
+binaryI Q A TcompatiblesameAddition
-binaryI Q A TcompatiblesameSubtraction
*binaryI Q A TcompatiblesameMultiplication
/binaryI Q A TcompatiblesameDivision
^binaryI QIntegerlike 1stPower
&binaryBBooleanBooleanLogical AND
|binaryBBooleanBooleanLogical OR
>binaryI QcompatibleBooleanGreater than
>=binaryI QcompatibleBooleanGreater or equal
<binaryI QcompatibleBooleanLess than
<=binaryI QcompatibleBooleanLess or equal
==binaryI Q A TcompatibleBooleanEqual
!=binaryI Q A TcompatibleBooleanNot equal

Here are some examples of each operator:

test

use std::debug::assert_eq; // used for testing

assert_eq([ -5,     0 - 5               ]); // Negation
assert_eq([ 5 + 6,  11                  ]); // Addition
assert_eq([ 5 - 6,  -1                  ]); // Subtraction
assert_eq([ 5 * 6,  30                  ]); // Multiplication
assert_eq([ 5 / 6,  0.83333333333333333 ]); // Division
assert_eq([ 5 ^ 6,  15625               ]); // Power
assert_eq([ true &  false, false        ]); // Logical AND
assert_eq([ true |  false, true         ]); // Logical OR
assert_eq([ 5 > 6,  false               ]); // Greater than
assert_eq([ 5 >= 6, false               ]); // Greater or equal
assert_eq([ 5 < 6,  true                ]); // Less than
assert_eq([ 5 <= 6, true                ]); // Less or equal
assert_eq([ 5 == 6, false               ]); // Equal
assert_eq([ 5 != 6, true                ]); // Not equal

Operators & Arrays

Some of the operators listed above can be used with arrays too. There result then is a new array with each value processed with the operator and the second operand.

See Array Operators for more information.


  1. B = Boolean, I = Integer, Q = Quantity, A = Array, T = Tuple

Model Expressions

Things change when an expression consists of models instead of just values. We call this a model expression:

test

std::geo2d::Rect(1cm) - std::geo2d::Circle(radius = 3mm);

In this expression which consists of a subtraction operation of the results of two calls to Rect and Circle.

Output
output

Building a group (using curly braces) of both operands and applying the builtin method subtract to it is equivalent to the above code:

test

use __builtin::ops::subtract;

{
    std::geo2d::Rect(1cm);
    std::geo2d::Circle(radius = 3mm);
}.subtract();
Output
output

The following operations can be applied to 2D or 3D models:

OperatorBuiltin OperationDescription
-__builtin::ops::subtractGeometrical difference
|__builtin::ops::unionGeometrical union
&__builtin::ops::intersectGeometrical intersection

Assignments

Whenever you use a more complex expression, it is often useful to store it behind a name so that it can be used once or multiple times elsewhere.

In µcad, stored values are always immutable, meaning that once a value has been stored behind a name, it cannot be reset in the same context. This is different from the variables known in other programming languages.

test

use std::math::sqrt;

a = 2cm;
b = 5cm; 
c = sqrt(a*a + b*b);

std::print("{c}");

Tip

If a name starts with an underscore (like _this_name) that suppresses any warning about if it is not in use.

There are several kinds of assignments for different purposes. A property for example is always related to a workbench so it can only be defined within one. Here is a complete list of all assignment types available in µcad and where they can be defined (✅) and where they are prohibited (❌):

TargetKey-wordSource FileModuleBuilding CodeFunc-tionInitial-izationInitial-izers
Value-
Model-
Constantconst
Publicpub
Propertyprop
Model-

Value Assignments

A value assignment stores a value by a name on the stack. They can be placed in source files, building code, module functions or workbench functions.

The following example defines the variable a which from then is a reserved name within the scope in which it was defined (locality). In this case the source code file itself:

test

// source code file is the scope of a and b

use std::debug::*;

a = 5;
b = a * 2;
assert_eq([ a, 5  ]);
assert_eq([ b, 10 ]);

Locality

If you place a value assignment into a scope (using brackets {}), the defined value is only available within that specific scope:

test

// source code file is the topmost scope #0

use std::debug::*;

// define a within scope #0
a = 5;
assert_eq([ a, 5 ]);

// scope #1
{
    // define b within scope #1
    b = a * 2;
    // of course b is available at this point
    assert_valid( b );

    // scope #2
    {
        // b is available in scope #2 because #2 is within #1
        assert_valid( b );
    }
}

// a is still available
assert_valid(a);
// b not known here anymore
assert_invalid(b);

Restrictions

No Shadowing

So-called “shadowing” (reusing a name) is prohibited. This restriction is highly intentional because µcad follows a concept of strict immutability1 of all value definitions.

test

a = 5;

{
    a = a * 2;   // this works because we are in a new scope
    std::debug::assert_eq([ a, 10 ]);
}

Another assignment of a variable with the same name without an additional scope is prohibited.

test

a = 5;      // warning: unused local
a = a * 2;  // error: a already defined in this scope

Not in modules

Value assignments are not available in modules:

test

mod my_module {
    a = 1; // error
}

Not in initialization code

Value assignments are not available in workbenches’ initialization code:

test

sketch MySketch() {
    a = 1;   // error
    init(_x : Scalar) {}
}

MySketch();

  1. Reusing names would undercut the ability to connect identifiers to values (e.g. when displaying).

Constant Assignments

Unlike values, constants are not stored on the stack but in the symbol table. For example this allows them to be accessed from within functions or workbenches in the same module where the constant is defined. Constants can be placed in source files, modules or initialization code.

test

const TEXT = "Hello";

mod my_module {
    
    // constant assignment
    const TEXT = "Hello my_module";

    // public function
    pub fn f() -> String {
        TEXT
    }

    // public workbench
    pub sketch MySketch(text: String) {
        std::debug::assert_eq([ TEXT, text ]);
    }
}

my_module::MySketch("Hello my_module");
std::debug::assert_eq([ my_module::f(), "Hello my_module" ]);
std::debug::assert_eq([ TEXT, "Hello" ]);

Additionally, constant assignments are permitted in the init code of a workbench, where value assignments are likewise prohibited.

test

sketch MySketch(text: String) {
    // constant assignment in initialization code
    const TEXT = "Hello";

    init() {
        text = TEXT;
    }

    std::print(text);
}

MySketch();

Restrictions

Uppercase naming

Constants are always written in UPPER_CASE.

test

const A = 1;        // ok
const a = 1;        // warning
const MyValue = 1;  // warning
const MY_VALUE = 1; // ok

Ambiguous Names

A constant cannot be defined within the same module or workbench twice.

test

mod module {
    const A = 5;
    const A = 1;  // error: A already defined in this module

    pub mod another_module {
        const A = 5;   // ok

        pub fn a() -> Integer { A }
    }

    pub sketch Sketch() {
        const A = 5;   // error: A is ambiguous
        const A = 5;   // error: A already defined in this workbench
    }
}

std::debug::assert_eq([ module::another_module::a(), 5 ]);
module::Sketch();

Not in building code

Constant assignments cannot be used in building code (the code below any initializers).

test

sketch MySketch() {
    init(_: Integer) {}
    const MY_CONST = 1;   // error: not allowed in building code
}
MySketch();

Constant assignments must be on top of the workbench code if initializers are not used.

test

sketch MySketch() {
    const MY_CONST = 1;   // allowed if no initializers
}
MySketch();

They cannot be placed below non constant assignments within in a workbench.

test

sketch MySketch() {
    _i = 5;                 // any non const code
    const MY_CONST = 1;     // error: const not allowed then
}
MySketch();

Not in function

Constant assignments cannot be used in functions.

test

fn f() {
    const MY_CONST = 1;     // error: not allowed in functions
}
f();

sketch MySketch() {
    fn f() {
        const MY_CONST = 1; // error: not allowed in workbench functions
    }
    f();
}
MySketch();

Not in initializers

Constant assignments cannot be used in initializers.

test

sketch MySketch() {
    init(_: Integer) {
        const MY_CONST = 1;   // error: not allowed in initializers
    }
}
MySketch();

Public Assignments

Public assignments provide a value to the inner and the outer of a module.

test

mod my_module {
    pub mod sub_module {
        pub TEXT = "Hello";
    }
}

std::print(my_module::sub_module::TEXT);

Restrictions

Not in workbenches

Using pub is not allowed within workbenches:

test

sketch MySketch() {
    pub TEXT = "Hello";  // error
    std::print(TEXT);
}

MySketch();

Not in functions

test

fn f() {
    const MY_CONST = 1;     // error: not allowed in functions
}
f();

Property Assignments

Property assignments define additional1properties of a workbench. The may appear anywhere within the building code of a workbench and can then be read from outside.

test

sketch MySketch(radius: Length) {
    prop diameter = radius * 2;
    std::geo2d::Circle(radius);
}
std::debug::assert_eq([ MySketch(5cm).diameter, 10cm ])

Restrictions

Not in source files

test

prop diameter = radius * 2; // error: not in source file

Not in functions

test

fn f() {
    prop diameter = radius * 2; // error: not in functions
}

f();

Not in initialization code

test

sketch MySketch(radius: Length) {
    prop diameter = radius * 2; // error: not in initialization code
    
    init() { radius = 1; }
    std::geo2d::Circle(radius);
}
std::debug::assert_eq([ MySketch(5cm).diameter, 10cm ])

Not in initializers

test

sketch MySketch(radius: Length) {
    init() { 
        radius = 1; 
        prop diameter = radius * 2; // error: not in initializer
    }
    std::geo2d::Circle(radius);
}
MySketch(5cm)

  1. Additional to properties which are automatically generated from a workbench’s building plan.

Model Assignments

Model assignments look like value assignments but instead having a value on the right side they store a model.

test

m = std::geo2d::Circle(radius = 10mm);               // assign the model of a circle into m
std::debug::assert_eq([ m.radius, 10mm ]);  // access property radius of m

Using a model as a value or vice versa does not work without further operations.

test

m = std::geo2d::Circle(radius = 10mm);   // assign the model of a circle into m
std::geo2d::Circle(radius = m);   // error: cannot use m as value

Restrictions

No Shadowing

Like with value assignments so-called “shadowing” (reusing a name) is prohibited. Another assignment of a variable with the same name without an additional scope is prohibited.

Not in modules

Model assignments are not available in modules:

test

mod my_module {
    a = std::geo2d::Circle(radius = 1mm);   // error
}

Not in initialization code

Model assignments are not available in workbenches’ initialization code:

test

sketch MySketch() {
    a = std::geo2d::Circle(radius = 1mm);   // error
    init(_x : Scalar) {}
}

MySketch();

Program Flow

µcad is a programming language designed for generating 2D and 3D models. It deliberately avoids common programming constructs such as loops or mutable variables, but instead offers an alternative concept we call Multiplicity.

This chapter explains how program flow in µcad works using these elements:

Start Code

A µcad program consists of one or multiple files.

Additional files can be added by the mod statement or by adding library paths of external modules with command line options.

Currently1, every µcad program starts with the original input file that was given at running microcad.

The top level code within this file is where microcad starts processing top to bottom.

test

// 1. Start code begins here.
use std::geo2d::*;

// 2. This is a module definition. It defines 'RADIUS' but doesn't generate any geometry.
mod my_inner {
    pub RADIUS = 10mm;
}

// 3. Start code continues here.
Circle( radius = my_inner::RADIUS );

  1. In future µcad will get a package management and will have projects and toml files.

Conditions

The if statement controls the program flow by the result of boolean conditions.

Structure of an if statement

In general, an if statement consists of the following elements in a fixed order:

  1. an initial if followed by the condition in form of an boolean expression
  2. a block of code (in { .. }) which is processed if the condition is true
  3. maybe one or more else if statements with alternative conditions and code blocks
  4. and last maybe an else statement followed by a code block which got executed when no other condition was true.

Conditions lead to different executions paths for different cases.

Here is a simple example:

test

use std::geo2d::*;

x = 0;  // output changes if you change that value

if x > 0 {
    Circle(radius = 5mm);
} else if x < 0 {
    Rect(10mm);
} else {
    Hexagon(5mm);
}
Output
test

if in workbenches

Inside a workbench block, an if statement can be used to select different shapes or constructions depending on input parameters. So in the following example all possible geometries are generated with parameter multiplicity and put side by side with the operation std::ops::align.

test

use std::ops::*;
use std::math::*;
use std::geo2d::*;

sketch MySketch(x: Integer) {
    if x > 0 {
        Circle(radius = 5mm)
    } else if x < 0 {
        Rect(10mm)
    } else {
        Hexagon(5mm)
    }
}

MySketch([-1,0,2]).align(X, 5mm);
Output
output

if in expressions

If statements can also be used as an expression, evaluating to the value from the chosen branch.

test

use std::ops::*;
use std::math::*;
use std::geo2d::*;

sketch MySketch(x: Integer) {
    outer = if x > 0 {
        Circle(radius = 5mm)
    } else if x < 0 {
        Rect(10mm)
    } else {
        Hexagon(5mm)
    };

    outer - Circle(radius = 3mm)
}

MySketch([-1,0,1]).align(X, 5mm);
Output
output

Calls

Workbenches and functions can get called, which just means there inner code gets executed. There are several types of calls which have some different effects or usage.

Call…ExampleInput(s)Output
functionvalue = my_function(..);parameter listValue
workbenchmodel = MySketch(..);parameter listModel1
operationnew_model = model.my_operation(..);Model1 &
parameter list
Model1

Calling Functions

A call of a function consists of just the identifier and an argument list. and the result is a value:

test

// call function -2 and store result in x
x = std::math::abs(-2);

// check value
std::debug::assert_eq( [x, 2] );

Calling Workbenches

Workbenches can be called in the same way as functions except that the result is a model.

test

// call sketch Circle with a size and store object node in s
s = std::geo2d::Circle(diameter = 2cm);

std::debug::assert_eq([ s.radius, 1cm ]);

Calling Operations

Operations are called in a different way because they are always attached to a model which come out of workbenches.

test

// call square with a size and store object node in s
s = std::geo2d::Circle(radius = 2cm);

// translate object s
s.std::ops::translate(x = 1cm);

Surely this looks better when using thw use statement.

test

// use translate
use std::ops::translate;

// call square with a size and store object node in s
s = std::geo2d::Circle(radius = 2cm);

// translate object s
s.translate(x = 1cm);

  1. including properties. ↩2 ↩3

Parameters & Arguments

Parameters

Important

Parameters in µcad are not positional (which means their order is irrelevant)!

Parameters in µcad always have a name which we often call identifier and a type.

test

// function with two parameters (`one` and `another`)
fn f( one: Integer, another: Length ) { 
    std::print("{one} {another}");
}

// call function with two parameters in arbitrary order
f(another = 2m, one = 1);

Arguments

Arguments are the values which are given in call. Each argument consists these elements:

  • an optional identifier in lower_snake_case
  • a value (e.g. 42, 3.1415, "Hello")
  • a type (e.g. Integer, Scalar, Length)
  • an optional unit that suits the type (e.g. mm for Length, for Area)

Parameter can have defaults. Then the notation changes and the type is deduced from the default value.

test

// function with two parameters (`one`, `another`, and `one_more`)
fn f( one: Integer, another = 2m, one_more: Area ) { 
    std::print("{one} {another} {one_more}");
}

// call function with two arguments.
// One matches by name another by type and one_more by default.
f(one = 1, 5m²);

There are several ways in which parameters can match arguments.

Argument Matching

To match call arguments with function or workbench parameters, µcad employs a process known as argument matching.

Important

Parameters in µcad are not positional (which means their order is irrelevant)!

Instead of having so-called positional arguments, µcad has named arguments, which means that every parameter and every argument must have an identifier. Like x is in the following example:

test

fn f(x: Length) -> Length { x*2 }

std::debug::assert_eq([ f( x = 10m ), 20m ]);

Fortunately there are some facilities for your convenience, like:

All those are created to feel natural when using them together. If some explanations in the following sections may sound complicated, you might just go with the examples and “get the feeling”.

Match Priorities

A single parameter can match an argument in several ways, each with a defined priority. These priorities become important when calling workbenches which support overloaded initialization.

Priority
⭣ high to low
MatchesExample ParametersExample Arguments
Empty ListEmpty arguments with empty parameters()()
IdentifierMatch argument identifier with parameter identifier(x: Scalar)(x=1)
Shortened IdentifierMatch argument identifier with shortened parameter identifier(max_height: Scalar)(m_h=1)
TypeMatch argument type with parameter type(x: Length)(1mm)
Compatible TypeMatch argument type with compatible parameter type(x: Scalar)f(1)
DefaultMatch parameter defaults(x=1mm)()

The match strategy is to try all priorities in order from highest (Empty) to lowest (Default) until all arguments match a parameter.

Match Empty List

Matches when both the arguments and parameters are empty.

test

fn f() {}   // no parameters

f();        // no arguments

Match Identifier

The following example demonstrates calling a function f with each argument specified by name:

test

fn f( width: Length, height: Length ) -> Area { width * height }

x = f(height = 2cm, width = 1cm);   // call f() with parameters in arbitrary order

std::debug::assert_eq([ x, 2cm² ]);

Match Short Identifier

A parameter can also be matched using it’s short identifier.

The short form consists of the first letter of each word separated by underscores (_).

test

fn f( width: Length, height: Length ) -> Area { width * height }

// use short identifiers
std::debug::assert_eq([ f(w = 1cm, h = 2cm), 2cm² ]);
// can be mixed
std::debug::assert_eq([ f(w = 1cm, height = 2cm), 2cm² ]);

Here are some usual examples of short identifiers:

IdentifierShort Identifier
parameterp
my_parameterm_p
my_very_long_parameter_namem_v_l_p_n
my_Parameterm_P
MyParameterM
myParameterm

Match Type

Nameless values can be used if all parameter types of the called function (or workbench) are distinct.

test

fn f( a: Scalar, b: Length, c: Area ) {}  // warning: unused a,b,c
// Who needs names?
f(1.0, 2cm, 3cm²);

Match Compatible Type

Nameless arguments can also be compatible with parameter types, even if they are not identical.

test

fn f( a: Scalar, b: Length, c: Area ) {}  // warning: unused a,b,c
// giving an integer `1` to a `Scalar` parameter `a`
f(1, 2cm, 3cm²);

Match Default

If an argument is not provided and its parameter has a default value defined, the default will be used.

test

fn f( a = 1mm ) {}  // warning: unused a
// a has default
f();

Mix’em all

You can combine all these methods.

test

fn f( a: Scalar, b: Length, c=2cm, d: Length) -> Volume {} // warning: unused a,b,c,d

// `a` gets the Integer (1) which is compatible to Scalar (1.0)
// `b` is named
// `c` gets it's default
// `d` does not need a name because `b` has one
f(b=2cm, 1, 3cm);

Inline Identifiers

In some cases, the parameter name is already included in the argument expression. If there is only one (or multiple identical) identifier(s) within an expression and it matches a parameter of the same type, that parameter will be automatically matched.

test

fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
f(x, y=2); // matches because argument `x` matches the name of parameter `x`

Even when using a more complex expression a unique identifier can match an argument:

test

fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
y = 2;
f(x * 2, y * y); // matches because `x` and `y` match parameter names `x` and `y`

test

fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
y = 2;
f(x * y, y * x); // error: cannot be matched because arguments are not unique.

Multiplicity

A core concept of µcad is called Argument Multiplicity which replaces loops known from imperative languages.

When working with multiplicities, each argument can be provided as an array of elements of a parameter’s type.

Each list element will then be evaluated for each of the array’s values. This way, we can intuitively express a call that is executed for each argument variant.

The following example will produce 4 rectangles at different positions:

test

r = std::geo2d::Rect(width = 2mm, height = 2mm);

r.std::ops::translate(x = [-4mm, 4mm], y = [-4mm, 4mm]);

Because in the above example x and y have two values each, the result are four (2×2) calls:

test

r = std::geo2d::Rect(width = 2mm, height = 2mm);

use std::ops::translate;
r.translate(x = -4mm, y = -4mm);
r.translate(x = -4mm, y = 4mm);
r.translate(x = 4mm, y = -4mm);
r.translate(x = 4mm, y = 4mm);

Normally, this would require two nested for loops. As you may imagine, multiplicity can be a powerful tool.

Match Errors

Missing Arguments

If you do not provide all parameters, you will get an error:

test

fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, z=3cm); // error: y is missing here

Too Many Arguments

When you provide all parameters but some are redundant, you will get a error too:

test

fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, y=2cm, v=5cm, z=3cm);  // error: Unexpected argument v

Ambiguous Arguments

If some arguments cannot be matched unambiguously to any of the parameters you will get an error.

test

fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, 5cm, 3cm);  // error: Missing arguments y and z

Types

The µcad type system consists of a group of builtin types. The type system is static, which means a every value has a fixed type which cannot be changed or overwritten.

Here is a complete list of the builtin types:

TypeDescriptionType DeclarationsExample Values
BooleanBoolean valueBooltrue, false
IntegerInteger value without unitInteger4, -1
QuantityFloating point value with or without unitScalar, Length, Area, Volume, Density, Angle, Weight0.5, 50%, -1.23e10, -4mm, 1.3m2, 2cm², 23.0e5deg, 100g
StringUTF-8 text stringString"Hello, World!"
ArrayList of values with common type[Integer][1,2,3], [1m,2cm,3µm]
TupleList of named values or distinct types(Length,Scalar,Bool), (x:Scalar,y:Length), (x:Scalar,Length)(4mm,4.0,true), (x=4.0,y=4mm), (x=4.0,4mm)
ModelGeometric 2D or 3D modelModelstd::geo3d::Cube(2mm)

Declaration

The examples in the table above declare the type explicitly. However, we can use units to declare the implicitly. Using units is recommended and what you get in return is that declarations are quite handy:

test

x: Length = 4mm;   // explicit type declaration
y = 4mm;           // implicit type declaration via units.

Declarations without any initializations are not allowed in µcad. Hence, the following example will fail:

test

x: Length;         // parse_error

However, for parameter lists in functions and workbenches, you can declare the type only but also pass a default value:

test

fn f(x = 4mm) {}       // use unit (with default)
fn f(x: Length) {}     // use type (without default)

Note

Find out more about what types are used for in the sections about argument matching and assignments.

Collections

Collections put many items into one type:

  • Arrays store multiple values of the same type
  • Tuples can store multiple values of different types which may or may not have names.

Arrays

Note

Arrays are quite involved in the multiplicity concept so you might want to read about this too.

Declaration

An array is an ordered collection of values with a common type.

test

a : [Integer] = [ 1, 2, 3, 4, 5 ];
b : [Scalar]  = [ 1.42, 2.3, 3.1, 4.42, 5.23 ];
c : [Length]  = [ 1.42mm, 2.3m, 3.1cm, 4.42cm, 5.23m ];
d : [String]  = [ "one", "two", "three", "four", "five" ];
e : [Model]   = [ std::geo2d::Circle(radius = 1cm),
                  std::geo3d::Sphere(radius = 1cm)
                ];

Of course these type declarations can be skipped (e.g. a = [ 1, 2, 3, 4, 5 ]).

Unit bundling

Array support unit bundling, which means the you can write a unit behind the brackets.

test

std::debug::assert_eq([ [1mm, 2mm, 3mm], [1, 2, 3]mm ]);

Single elements of the array can have special units of the same type.

test

std::debug::assert_eq([ [1mm, 2m, 3mm], [1, 2m, 3]mm ]);

Range Arrays

Alternatively a range expression can be used to generate an array with consecutive values.

Important

µcad ranges include start and end point! This is different in many other programming languages. So in µcad [1..3] results in [1,2,3].

test

std::debug::assert_eq([[1..5], [1,2,3,4,5]]);
std::debug::assert_eq([[-2..2], [-2,-1,0,1,2]]);

The order of the endpoints of a range is important.

test

[6..1];  // error
[2..-2];  // error

Only Integer can be used as endpoint.

test

[1.0..2.0];  // parse_error

Array Operators

Arrays support the following operators.

OperatorDescriptionExample
+add value to every element[1, 2] + 2
-subtract value from every element[1, 2] - 2
*multiply every element with a value[-1.0, 2.0] * 2.0
/divide every element by a value[1.0, 2.0] / 2.0
-negate every element-[ 1, 2 ]
!invert every element![ true, false ]

test

use std::debug::assert_eq;

assert_eq([ [1, 2] + 2,        [3, 4] ]);
assert_eq([ [1, 2] - 2,        [-1, 0] ]);
assert_eq([ [-1.0, 2.0] * 2.0, [-2.0, 4.0] ]);
assert_eq([ [1.0, 2.0] / 2.0,  [0.5, 1.0] ]);
assert_eq([ -[-1.0, 1.0],      [1.0, -1.0] ]);

test

std::debug::assert_eq([ ![true, false], [false, true] ]);

Tuples

A tuple is a collection of values, each of which can be of different type. Typically, each value is paired with an identifier, allowing a tuple to function similarly to a key-value store.

test

use std::debug::assert_eq;

tuple = (width=10cm, depth=10cm, volume=1l);

assert_eq([ tuple.width, 10cm ]);
assert_eq([ tuple.depth, 10cm ]);
assert_eq([ tuple.volume, 1l ]);

assert_eq([ tuple, (width=10cm, depth=10cm, volume=1l) ]);

Partially Named Tuples

A tuple may lack identifiers for some or even all of its values. In such cases, these unnamed values within the tuple must all be of different types.

test

(10cm, 10cm², 1l);

Otherwise, they would be indistinguishable since the values in a tuple do not adhere to a specific order.

Arbitrary Order

The order of values have no consequences for equality.

test

// these tuples are equal
std::debug::assert_eq([ (1l, 10cm, 10cm²), (10cm, 10cm², 1l) ]);

Arbitrary Units

Different units of values have no consequences for equality.

test

// these tuples are equal
std::debug::assert_eq([ (1000cm3, 100mm, 0.01m²), (10cm, 100cm², 1l) ]);

Ambiguous Elements

Either names or types must be unique in a tuple.

test

(10cm, 10mm, 1m);  // error: ambiguous type Length

Tuple Aliases

Some named tuples are used frequently and thus they have aliases: Color, Vec2 and Vec3. This feature is also called named-tuple duck-typing.

Color

A named tuple with the fields r, g, b and a will be treated as a color with red, green, blue and alpha channel. You can use the type alias Color to deal with color values.

test

color: Color = (r = 100%, g = 50%, b = 25%, a = 100%);

std::debug::assert(color.r == 100%);

Vec2

A named tuple with the fields x and y will be treated as a two-dimensional vector. You can use the type alias Vec2.

test

v: Vec2 = (x = 2.0, y = 3.0);

std::debug::assert(v.x + v.y == 5.0);

Vec3

A named tuple with the fields x, y and z will be treated as a three-dimensional vector. You can use the type alias Vec3.

test

v: Vec3 = (x = 2.0, y = 3.0, z = 4.0);

std::debug::assert(v.x + v.y + v.z == 9.0);

Tuple Operators

Tuples support the following operators.

OperatorDescriptionExample
+add each element(x=1, y=2) + (x=3, y=4)
-subtract each element(x=1, y=2) - (x=3, y=4)
*multiply each element(x=1, y=2) * (x=3, y=4)
/divide each element(x=1, y=2) / (x=3, y=4)
-negation of each element-(x=1, y=2)
!inversion of each element!( true, false )

test

std::debug::assert_eq([ (x=1, y=2) + (x=3, y=4), (x=4, y=6) ]);
std::debug::assert_eq([ (x=2, y=3) - (x=1, y=4), (x=1, y=-1) ]);
std::debug::assert_eq([ (x=1.0, y=2.0) * 2, (x=2.0, y=4.0) ]);
std::debug::assert_eq([ (x = 1.0, y = 2.0) / 2, (x = 0.5, y = 1.0)]);
std::debug::assert_eq([ -(x = 1.0, y = 2.0), (x = -1.0, y = -2.0)]);

Tuple Mismatch

Names or types must match like the element count.

test

(x=1, y=2) + (x=3, z=4);      // error: mismatch (x, y) + (x, z)
(x=1, y=2) + (x=3mm, y=4mm);  // error: mismatch (Integer, Integer) + (Length, Length)
(x=1, y=2) + (x=3, y=4, z=5); // error: mismatch unexpected z

Quantities

The term quantities bundles a set of quantity types which are basically floating point1 values with physical units (e.g. Meter, Liters,…) attached.

test

// declare variable `height` of type `Length` to 1.4 Meters
height = 1.4m;

// use as *default* value in parameter list
fn f( height = 1m ) {}

// calculate a `Length` called `width` by multiplying the
// `height` with `Scalar` `2` and add ten centimeters
width = height * 2 + 10cm;

  1. see wikipedia about floating point

Quantity Types

The following quantity types are currently supported in µcad:

TypeMetric UnitsImperial Units
Scalar% or none-
Lengthµm, mm, cm, min or ", ft or ', yd
Angle° or deg, grad, turn,rad
Areaµm²,mm²,cm²,in², ft² , yd²
Volumeµm³, mm³,cm³,,ml,cl,l, µlin³, ft³ , yd³
Densityg/mm³-
Weightg, kglb, oz

Tip

Special characters like µ, °, ², and ³ have ASCII1 replacements you may use if your keyboard misses these letters (see this appendix for more information).

Scalar

The type Scalar contains a floating number (without any unit) and must be written with at least one decimal place (or in percent).

test

zero = 0;
pi =  3.1415;
percent = 55%;

Length

Length is used to describe a concrete length.

test

millimeters = 1000mm;
centimeters = 100cm;
meters = 1m;
inches = 39.37007874015748in;

std::debug::assert_eq([ millimeters, centimeters, meters, inches ]);

Angle

Angles are used with rotations and in constrains when proving measures.

test

pi = std::math::PI;
radian = 1rad * pi;
degree = 180°;
degree_ = 180deg;
grad = 200grad;
turn = 0.5turns;

std::debug::assert_eq([ degree, degree_, grad, turn, radian ]);

Area

An Area is a two-dimensional quantity. It is the result when multiplying two Lengths.

test

a = 3cm;
b = 2cm;
area = a * b;
std::debug::assert(area == 6cm²);

Here is an example of how to use different areal units:

test

square_millimeter = 100000mm²;
square_centimeter = 1000cm2;
square_meter = 0.1m²;
square_inch = 155in²;

std::debug::assert_eq([ square_millimeter, square_centimeter ]);

Volume

A Volume is a three-dimensional quantity. It is the result when multiplying three Lengths.

test

a = 3mm;
b = 2mm;
c = 4mm;

volume = a * b * c;

std::debug::assert(volume == 24mm³);

Here is an example for units:

test

cubic_millimeter = 1000000.0mm³;
cubic_centimeter = 100.0cl;
cubic_meter = 0.001m3;
cubic_inch = 61.0237in³;
liter = 1.0l;
centiliter = 100.0cl;
milliliter = 1000.0ml;

std::debug::assert_eq([ cubic_millimeter,
                        cubic_centimeter,
                        cubic_meter,
                        centiliter,
                        milliliter
                        ]);

Density

Currently Density has not specific use in µcad and only can use unit g/mm³.

test

gramm_per_square_centimeters = 19.302g/mm³;

Weight

Weights can be calculated by applying volumes to materials.

test

gram = 1000.0g;
kilogram = 1.0kg;
pound = 2.204623lb;

std::debug::assert_eq([ gram, kilogram ]);

  1. see wikipedia about ASCII

Quantity Operators

Quantity types support most operators which primitive types do. See section operators in expressions for a complete list

The only difference about quantity operators is, that they calculate units too, where units remain flexible.

test

use std::debug::assert_eq;

assert_eq([ 6cm * 2cm, 12cm²  ]);
assert_eq([ 6cm / 2cm, 3 ]);
assert_eq([ 6cm + 2cm, 80mm ]);
assert_eq([ 6cm - 2cm, 0.04m ]);

Primitive Types

Bool

Boolean is the result type of boolean expressions, which may be true or false.

test

std::debug::assert(true != false);

Boolean values can be combined with or and and operators:

test

std::debug::assert_eq([true or false, true]);
std::debug::assert_eq([true and false, false]);

std::debug::assert_eq([4 == 4, true]);
std::debug::assert_eq([4 == 5, false]);
std::debug::assert_eq([4 == 5 or 4 == 4, true]);
std::debug::assert_eq([4 == 5 and 4 == 4, false]);

Integer

The type Integer contains an integer number.

test

i = 3;

String

Strings are mostly used for rendering text.

test

text = "Hello µcad!";
std::debug::assert_eq([std::count(text), 11]);

// logging
std::print(text);

Matrix

Matrix types are built-in types and can be defined as:

  • Matrix2: A 2x2 matrix, commonly used for 2D rotations.
  • Matrix3: A 3x3 matrix, commonly used for rotations.
  • Matrix4: A 4x4 matrix, commonly used for affine transformations.

Note: Currently, matrices are used only internally and cannot be instantiated from µcad code.

Format Strings

Format strings are strings which include a special notation to insert expressions into a text.

test

std::debug::assert_eq([ "{2+5}", "7" ]);

Usually they are used to insert parameters into text:

test

fn print_length( length: Length ) {
    std::print("{length}");
}

print_length(7mm);

Bad Expression in Format String

If a format string expression cannot be evaluated, you will get an error.

test

fn print_length( length: Length ) {  // warning: unused length
    std::print("{size}");            // error: size is not known
}

print_length(7mm);

Documentation

Code Comments (//, /* ... */)

By using // or /* ... */ you may insert comments within the code.

test

// This is a line comment...

/* This is a block comment */

/* Block comments my have...
   ...multiple lines.
*/

std::print("Hello, ");   // Line comments can be appended to a line
std::print( /* Block comments can be placed almost anywhere */ "world!");

Doc Comments (///, //!)

You may also use comments to attribute your code with documentation. There are outer (///) and inner //! doc comments.

Outer doc comments (///)

By placing a comment with /// above a symbol definition you can attribute your code with documentation. Markdown may be used to shape sections or format text.

test

/// A function which returns what it gets.
///
/// It simply returns the **same** value...
///
/// ...as it got from the *parameter*.
///
/// ## Arguments
///
/// - `n`: input value
///
/// ## Returns
///
/// Output value.
fn f( n: Integer ) -> Integer { n }

// usual comment for non-symbols
f(1);

Till the first empty line text will be interpreted as a summary for documentation, all other lines will build the detailed description.

The above function f will be documented with the following markdown output:

A function which returns what it gets.

It simply returns the same value…

…as it got from the parameter.

Arguments

  • n: input value

Returns

Output value.

Inner doc comments (//!)

Inner doc comments are used to document code inside a source file:

test

//! This inner doc comment documents the whole source file.

fn f( n: Integer ) -> Integer { n }

f(1);

Attributes

Attributes are syntax elements used to attach metadata to models or control output during rendering and exporting. While they do not alter the geometry of the model tree itself, they provide essential instructions for viewers and exporters.

Attribute Categories

TypeSyntax ExamplePurpose
Metadata#[color = "red"]Attaches key-value pairs for organizational or aesthetic metadata.
Command#[export("mesh.stl")]Triggers specific actions, like file generation or render settings.

Outer vs. Inner Attributes

The primary difference lies in the scope of what the attribute affects.

Outer Attributes (#[...])

Outer attributes are placed outside and before a statement. They apply to the specific node or block immediately following them.

test

#[color = "#FF0000"]
{
    std::geo2d::Circle(r = 20mm);
    std::geo2d::Rect(size = 20mm).std::ops::translate(x = 20mm);
}

Inner Attributes (#![...])

Inner attributes are separate statements inside a block or source file. They apply to the “parent” model they reside in, affecting everything within that scope. The following code is semantically equivalent to the outer attribute example above:

test

{
    #![color = "#FF0000"]

    std::geo2d::Circle(r = 20mm);
    std::geo2d::Rect(size = 20mm).std::ops::translate(x = 20mm);
}

Pro Tip: Inner attributes are particularly useful at the very top of a file to set global parameters without needing to wrap the entire script in a group body.

Metadata attributes

Metadata attributes allow you to store and retrieve arbitrary key-value pairs. Each attribute name must be unique within its scope to avoid conflicts. This metadata is used by viewers for rendering or by exporters when writing output to a file.

Key Properties

  • Non-Destructive: Metadata does not change the mathematical definition of the geometry, only how it is processed or displayed.
  • Inheritance: Depending on the exporter and renderer, metadata on a model may be inherited by its children unless overridden.

color attribute

The color attribute attaches a color to a model node. In a viewer and for some exporter, the model will be rendered in the specified color.

test

#[color = "#FFFFFF"]
c = std::geo2d::Circle(r = 42.0mm);

std::debug::assert_eq([c#color, (r = 1.0, g = 1.0, b = 1.0, a = 1.0)]);

You can access the color property using the # accessor.

resolution attribute

The resolution attribute defines the rendering fidelity of the model. It determines how a smooth mathematical curve is sampled and converted into discrete segments or triangle.

  • Default: Typically 0.1mm.
  • Percentage: Using a percentage scales the fidelity relative to the default. Note that higher resolution means a smaller step size:
    • 100% = 0.1mm (Default)
    • 200% =0.05mm` (Finer detail)
    • 50% = 0.2mm (Coarser detail)

This means the circle in the example below will be rendered with a resolution 0.05mm.

test

#[resolution = 200%]
c = std::geo2d::Circle(r = 42.0mm);

std::debug::assert_eq([c#resolution, 200%]);

Command attributes

Command attributes are used to control the viewer or renderer with specific commands. They do not alter the geometry, but they can have external effects, like writing something into a file.

export command

The export command annotates a model with a filename and optional export parameters.

Assume you have created a model and want to export it to a specific file:

test

#[export("rect.svg")] // Will be exported to `rect.svg`
std::geo2d::Rect(42mm);

#[export("circle.svg")] // Will be exported to `circle.svg`
std::geo2d::Circle(r = 42mm);

When this µcad source file is exported via the command line interface, the two models will be exported to rect.svg and circle.svg. This way, you can export multiple file at once.

The corresponding exporter is detected automatically depending on the file extension.

An export command can only be used in top level code within a source code. It cannot be used within modules, workbenches and functions.

Import data

Use can import data via std::import function.

Note: This WIP. Currently, only tuples from a TOML file can be imported.

TOML import

Assuming, you have the following data in a TOML file example.toml:

# file: example.toml
[M6]
diameter = 6.0
pitch = 1.0

[M10]
diameter = 10.0
pitch = 1.5

You can then load the TOML file by using std::import and access its values:

test

data = std::import("example.toml");

std::debug::assert_eq([data.M10.diameter, 10.0]);
std::debug::assert_eq([data.M6.pitch, 1.0]);

Included Libraries

With microcad comes with a builtin and a standard library.

µcad Builtin Library

The µcad builtin library is written in Rust (and still a little C) and brings mathematical calculation functions, model processing and rendering capabilities into µcad.

The µcad builtin library is available in the global module __builtin and is self documented in a reference book.

test

use __builtin::print;

print("Hello, µcad builtin library!");

µcad Standard Library

The µcad standard library is written in µcad and provides a convenient interface to the µcad builtin library.

The µcad standard library is available in the global module std and is self documented in a reference book.

test

use std::print;

print("Hello, µcad standard library!");

Appendix

Coding Style

The µcad language has a styling convention which shall be used.

Naming convention

ElementExampleFormat
Constantconst MY_CONST = 1;UPPER_SNAKE_CASE
Typex: Length = 1;PascalCase
Sketchsketch MySketch() {}PascalCase
Partpart MyPart() {}PascalCase
Operationop my_op() {}snake_case
Valuemy_var = 1;snake_case
Publicpub my_var = 1;snake_case
Modulemod my_lib {}snake_case
Functionfn my_func() {}snake_case
Modelmy_modelsnake_case
Propertymy_model.my_propsnake_case
Attribute#[my_attribute]snake_case

Keywords

The following keywords are reserved and cannot be used as identifier.

KeywordDescription
__builtinbuiltin module
__plugin(reserved)
_underscore identifier
andlogical and
Angleangle quantity type
Areaarea quantity type
aspart of use-as
assembly(reserved)
Boolboolean type
Colorcolor type
constconstant definition prefix
Densitydensity quantity type
elsepart of if-else
enum(reserved)
falseboolean constant
fnfunction definition prefix
ifpart of if-else
initworkbench initializer
Integerinteger type
Lengthlength quantity type
match(reserved)
material(reserved)
Matrixmatrix type
modmodule definition prefix
Modelmodel type
opoperation definition prefix
orlogical or
part3D workbench
propproperty definition prefix
pubpublic visibility prefix
returnreturn statement
Scalarfloating point type
Size22D size vector
sketch2D workbench
Stringtext string
struct(reserved)
trueboolean constant
unit(reserved)
useuse statement
Vec22D vector
Vec33D vector
Volumevolume quantity type
Weightweight quantity type
xorlogical xor

Special Characters

µcad is already using a Unicode character in its name and it allows even more Unicode to improve readability of scientific unit notations within the source code.

Sadly the majority of keyboards do not support some of them. In this case you may easily replace the special characters like shown in the following table.

Original CharacterReplacementExampleEquivalentUsage
µm, uµcadmcad,ucadfile extension, markdown testing
µuµm, µlum, ulmicro prefix in metric units
°deg3degangle unit
²23m²3m2area units
³33m³3m3volume units

Test List

The following table lists all tests included in this documentation.

172 tests have been evaluated with version 0.3.0 of microcad.

Click on the test names to jump to file with the test or click the buttons to get the logs.

ResultSourceName
testargument_match_auto
testargument_match_auto_err
testargument_match_default
testargument_match_empty
testargument_match_id
testargument_match_mix
testargument_match_short
testargument_match_single_identifier
testargument_match_type
testargument_match_type_compatible
testarray_operation_bool
testarray_operations
testarray_unit_bundling
testarray_unit_bundling_except
testarrays
testassignment
testassignment_model_module
testassignment_model_workbench
testassignment_module
testassignment_shadow
testassignment_shadow_scope
testassignment_value
testassignment_value_scope
testassignment_workbench
testattributes_color
testattributes_export
testattributes_precision
testboolean
testboolean_literal
testbuilding_plan
testbuilding_plan_defaults
testcall_function
testcall_match
testcall_model
testcall_model_use
testcall_workbench
testcode
testcode_between_initializers
testcode_post_init
testcomment
testconst_assignment_building_code
testconst_assignment_fn
testconst_assignment_init
testconst_assignment_mod
testconst_assignment_shadow
testconst_assignment_uppercase
testconst_assignment_workbench
testconst_assignment_workbench_code
testconst_assignment_workbench_code_wrong
testexample
testexpression_boolean
testexpression_literals
testexpression_model
testexpression_multiply
testexpression_quantity
testfile_modules_main
testfile_modules_second
testformat_string
testformat_string_err
testformat_string_value
testfunction_call
testfunction_conditional_result
testfunction_default
testfunction_param_return
testfunction_result
testfunction_return
testif_expression
testif_statement
testif_statement_sketch
testillegal_workbench_statement_mod
testillegal_workbench_statement_return
testillegal_workbench_statement_sketch
testinit_code_no_building_plan
testinit_property
testinline_mod
testinner_attributes
testinner_doc_comment
testinput
testinteger_literal
testlib_builtin
testlib_std
testmatch_ambiguous
testmatch_errors
testmatch_warnings
testmeasure
testmissed_property
testmod
testmod_example
testmodel_assignment
testmodel_assignment_cross
testmodel_expression
testmodel_expression_builtin
testmultiplicity_arrays
testnamed_tuple_access
testno_building_plan_in_initializers
testno_building_plan_same_name
testno_building_plan_same_name_different_type
testno_multiplicity
testnone
testop_example
testoperator_examples
testouter_attributes
testouter_doc_comment
testparameter
testparameter_default
testparameters
testpart_2d
testpart_basic
testpart_declaration
testpre_init_code
testprop_assignment
testprop_assignment_fn
testprop_assignment_init
testprop_assignment_initializer
testprop_assignment_source
testproperty
testproperty_no_prop_in_init_code
testproperty_no_prop_in_initializer
testproperty_wrong
testpub_assignment
testpub_assignment_fn
testpub_assignment_workbench
testquantity_literal
testquantity_types_number_literals
testrange_expressions
testrange_expressions_bad_order
testrange_expressions_bad_type
testreturn_twice
testscalar_literal
testsketch_3d
testsketch_basic
testsource_file_2D
testsource_file_3D
testsource_file_mixed
teststart
teststring_literal
testtoml_import
testtuple_error_mismatch
testtuple_operations
testtypes_bundles_functions
testtypes_def_vs_decl
testtypes_named_tuple_color
testtypes_named_tuple_vec2
testtypes_named_tuple_vec3
testtypes_no_declaration
testtypes_primitive_bool
testtypes_primitive_integer
testtypes_primitive_string
testtypes_quantity_angle
testtypes_quantity_area
testtypes_quantity_area_units
testtypes_quantity_density_units
testtypes_quantity_length
testtypes_quantity_operators
testtypes_quantity_scalar
testtypes_quantity_volume
testtypes_quantity_volume_units
testtypes_quantity_weight
testunnamed_tuple
testunnamed_tuple_ambiguous
testunnamed_tuple_order
testunnamed_tuple_units
testuse
testuse_all
testuse_as
testuse_as_module
testuse_module
testuse_statement_pub
testworkbench_example
testworkbench_fn_prop
testworkbench_init_prop
testworkbench_pub