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 microcad. 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 primary consists of the following core elements:

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

Source Files

Source files are simply files which contain µcad code. Such files might 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 elements 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. Modules help manage scope and control visibility (e.g., using pub to make items public available).

In short:

  • Modules define a namespace for your code.
  • They can be nested to create hierarchies,
  • declared with the mod keyword (internal modules)
  • or in separate files (external modules).
  • Control what is exposed to the outside world using pub.

Example

The following example shows two nested internal 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;

Internal Modules

Internal modules are modules which have been 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 value
    pub VALUE = 1;
}

Module names are always written in lower_snake_case.

The following statements can be listed in a 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 { }
ifprocess conditionsif x > 1 { }
useuse elements from other modulesuse m;
calluse functions and workbenchesf();
commentfor documentation// comment

External Modules

External modules are external source files.

For example if you put a second file beside your main source code file, you can easily import this second file.

test

mod second;
second::f(); 

test

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

By using mod second, in the first source file, microcad searches for either a file called second.µcad or second/mod.µcad and loads it into a module.

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

Use Statements

When including code from other modules or other source files the use of fully qualified names (e.g. std:geo3d::Sphere or std:geo3d::Cube) may produce much boiler plate code when using them often. The powerful use statement solves this problem and gives you an elegant method to import code from other sources.

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

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

test

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

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

Alternatively you can use the whole module geo3d at once and would get rid of the std:: part within the names:

test

use std::geo3d;

geo3d::Sphere(radius = 40mm);

Use As Statement

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

If name conflicts occur a way to deal with this is to explicitly name the identifier with use as:

test

use std::geo3d::Sphere as Disk;

Disk(radius = 4mm);

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

test

use std::geo3d as space;

space::Sphere(radius = 4mm);

Use All 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.

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 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 bit1 integersigned integer
Scalar64 bit1 floatsigned floating point
Boolean1 bit boolboolean
StringUTF-8Text
Quantities64 bit1 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.


  1. On 64 bit systems. ↩2 ↩3

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.

Start Code

A µcad program consists of one or multiple files.

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.

test

// start code begins here
use std::geo2d::*;

mod my_inner {
    pub RADIUS = 10mm;
}

Circle( radius = my_inner::RADIUS );
// start ends begins here

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

Conditionals

The if statement can control the program flow by the result of conditions.

In general an id statement consists of the following elements in 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 replace loops like they are known from other 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
ScalarFloating point value without unitScalar0.5, 50%, -1.23e10
QuantityFloating point value with unitLength, Area, Volume, Density, Angle, Weight-4mm, 1.3m2, 2cm², 23.0e5deg
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 just 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 solved 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);

Models

Builtin Measures

Measures are builtin methods you can use with 2D or 3D objects.

The following example calculates the area of a circle by using the measure area:

test

__builtin::debug::assert_eq([
    // use measure area() on a circle
    std::geo2d::Circle(radius=10mm).area(),

    // circle area formula for comparison
    10mm * 10mm * std::math::PI
]);

Currently it is not possible to declare measures in µcad.

2D Measures

Builtin measures that can be used on 2D objects.

MeasureOutput QuantityDescription
area(..)Areaarea
circum(..)Lengthcircumference
center(..)(x:Length, y:Length)geometric center
size(..)(width:Length, height:Length)extents
bounds(..)(left:Length, right:Length, top:Length, bottom:Length)bound (from center)

3D Measures

Builtin measures that can be used on 3D objects.

MeasureOutput QuantityDescription
area(..)Areasurface area
center(..)(x:Length, y:Length, z:Length)geometric center
size(..)(depth:Length, width:Length, height=Length)extents
bounds(..)(front: Length, back: Length, left:Length, right:Length, top:Length, bottom:Length)bound (from center)
volume(..)Volumevolume

Documentation

Code Comments

By using // or /* and */ 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.

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.

Attributes

Attributes are syntax elements that can be used to attach arbitrary data to model nodes.

The attributes will not change the model geometry itself, but might change its appearance when if they are used for viewers or exporters. There can be multiple attributes for a node, but each attribute needs to have a unique ID.

Simple example

Let’s define a model node c.

When viewed or exported, model node c will have a red color, because the color attribute will be set:

test

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

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

Syntax

Syntactically, an attribute consists of # prefix and an item.

  • Name-value pair: #[color = "#FF00FF"], #[resolution = 200%]. Store and retrieve arbitrary values. The name has to be unique.

  • Calls: #[svg("style = fill:none")]. Control the output for a specific exporter.

  • Commands: #[export = "test.svg"], #[measure = width, height]. A certain command to execute on a model, e.g. for export and measurement. Multiple commands are possible.

Color attribute

The color attribute attaches a color to a model node.

In viewer and when exported, the model will be drawn 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)]);

Resolution attribute

The resolution attribute sets the rendering resolution of the model. The model will be rendered in with 200% resolution than the default resolution of 0.1mm. This means the circle 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%]);

Export command

If you have created a part or a sketch and want to export it to a specific file, you can add an export command:

test

#[export = "circle.svg"]
c = std::geo2d::Circle(r = 42.0mm);

The exporter is detected automatically depending on the file extension.

See export for more information.

Export nodes

When a µcad file is processed by the interpreter, you can export the resulting nodes in a specific file format.

Export via CLI

To export a µcad file via the CLI, the most simple form is:

µcad export myfile.µcad # -> myfile.svg

You can also give an optional custom name with a specific format:

microcad-cli export myfile.µcad custom_name.svg # -> custom_name.svg

Default formats

The output format depends on the kind of node:

  • SVG is the default format for sketches.
  • STL is the default format for parts.
  • If the content is mixed, there will be an error, unless you mark different nodes with export attributes (see next section).

If you call the command line:

microcad-cli export my_sketch.µcad # This is a sketch and will output `my_sketch.svg`
microcad-cli export my_part.µcad # This is a part and will output `my_part.stl`

Export specific nodes via attributes

Assuming, you have two sketches and want to export each in a specific file. You assign an export attribute with a filename to each sketch. If you omit the file extension, the default export format will be picked automatically.

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

In the CLI, you can select the node specifically:

microcad-cli export myfile.µcad --list # List all exports in this file: `rect, circle.svg`.
microcad-cli export myfile.µcad --target rect  # Export rectangle node to `rect.svg`

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 come two out of the box libraries.

µ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!");

µ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!");

Debugging

Verification

µcad provides several builtin functions that help you to avoid bad input parameters.

Assert

Assertions define constrains of parameters or cases and they bring any rendering to fail immediately.

one form of assertion is a function which gets an expression. If the expression computes to false a compile error will occur at that point.

test

std::debug::assert(true, "You won't see this message");

test

std::debug::assert(false, "this assertion fails"); // error

Error

test

std::log::error("this should not have happened"); // error

Todo

todo() is like error() but aims at reminding you to finish code later.

test

a = 0;

if a == 0 {
    std::log::info("a is zero");
} else {
    std::log::todo("print proper message");
}

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 it’s name and it allows some more 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.

175 tests have been evaluated with version 0.2.20 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
testattributes_simple_example
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
testdoc_comment
testexample
testexport_attributes
testexpression_boolean
testexpression_literals
testexpression_model
testexpression_multiply
testexpression_quantity
testexternal_modules_main
testexternal_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
testinput
testinteger_literal
testinternal_mod
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
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
testverify_assert
testverify_assert_fail
testverify_error
testverify_todo
testworkbench_example
testworkbench_fn_prop
testworkbench_init_prop
testworkbench_pub