yapCAD DSL Reference

Looking for how to write DSL, not just look up a function? Start with the DSL Language Guide (mental model, idioms) and the DSL Tutorial (worked example). This Reference is the complete catalog — your day-to-day lookup. Learning path: Guide → Tutorial → Reference.

A domain-specific language for parametric CAD design with full type safety and provenance tracking.

Quick Start

module my_design

command MAKE_BOX(width: float, height: float, depth: float) -> solid:
    result: solid = box(width, height, depth)
    emit result

CLI Usage

# Check DSL syntax and types
python -m yapcad.dsl check myfile.dsl

# List available commands
python -m yapcad.dsl list myfile.dsl

# Run a command
python -m yapcad.dsl run myfile.dsl COMMAND_NAME

# Run with parameters
python -m yapcad.dsl run myfile.dsl COMMAND_NAME --param width=10.0 --param height=5.0

# Export to STEP file
python -m yapcad.dsl run myfile.dsl COMMAND_NAME --output result.step

# Export to package
python -m yapcad.dsl run myfile.dsl COMMAND_NAME --package output.ycpkg

# Increase recursion limit for deeply recursive designs
python -m yapcad.dsl run myfile.dsl COMMAND_NAME --recursion-limit 500
# Or via environment variable:
YAPCAD_DSL_RECURSION_LIMIT=500 python -m yapcad.dsl run myfile.dsl COMMAND_NAME

Syntax Overview

yapCAD DSL uses Python-like syntax:

  • Colons (:) after command signatures and control flow

  • Indentation defines blocks

  • # for comments

  • Strong static typing with explicit type annotations

  • Both let name: type = value and name: type = value for variable declaration

Module Structure

module module_name

# Optional imports (future feature)
use other_module
use package.submodule as alias

# Helper command (lowercase name, not exported)
command make_helper(x: float) -> solid:
    # ...
    emit result

# Exported command (UPPERCASE name, appears in CLI)
command MAKE_PART(param: float, param2: float = 10.0) -> solid:
    # body
    emit result

Note: Commands with UPPERCASE names are exported and visible to dsl list. Commands with lowercase names are helpers usable within the module but not directly callable from CLI.

Parameter Decorators

Parameters in a command definition may carry a @ui(...) decorator that provides viewer and widget hints to the yapCAD workbench. The decorator is purely informational — the evaluator ignores it completely, so it has no effect on geometry output or type-checking.

@meta(...) — Command Output Metadata

Placed on a command (or def) definition, before the parameter list. Multiple @meta decorators on the same command are merged — later decorators win on key collision.

@meta(assembly.joint_kind="revolute", assembly.surface="flange_face")
@meta(operation.kind="cut", operation.feature_kind="pocket")
command MAKE_POCKET(
    depth: float @ui(widget="slider", min=1.0, max=50.0) = 10.0
) -> solid:
    ...

Key syntax

Keys may be plain identifiers or dotted namespace paths:

Form

Example

Meaning

Plain

label="Hinge bracket"

Unnamespaced, free-form

Dotted

assembly.joint_kind="revolute"

Namespaced to the v1.1 assembly namespace

Dotted

operation.kind="cut"

Namespaced to the v1.1 operation namespace

Type-keyword words (surface, solid, float, …) are valid key segments even though the lexer classifies them as type tokens (e.g. assembly.surface).

Values

Same literal rules as @ui: strings, integers, floats, booleans, unary-minus numerics, and lists of the above. Non-literals are stringified.

Namespace conventions (v1.1)

Prefix

Namespace

v1.1 helpers

assembly.*

Assembly metadata

get_assembly_metadata(), set_assembly()

operation.*

Operation/machining

get_operation_metadata(), set_operation()

(none)

Free-form

Stored as-is

v1.1 enum vocabularies (values outside these sets raise ValueError at apply time):

Key

Valid values

assembly.joint_kind

axial, radial, mixed, none

operation.kind

subtract, intersect, union

operation.policy

strict, warn, ignore

operation.feature_kind

access_panel, vent, parachute_door, fastener_through, wire_pass, channel, pocket, other

Surfaced through the service API

The /dsl/commands endpoint includes meta_hint in each command object when one or more @meta decorators are present:

{
  "name": "MAKE_POCKET",
  "params": [...],
  "meta_hint": {
    "assembly.joint_kind": "revolute",
    "assembly.surface": "flange_face",
    "operation.kind": "cut",
    "operation.feature_kind": "pocket"
  }
}

Evaluator behaviour

@meta is evaluator-transparent — it has no effect on geometry evaluation, type-checking, or the emitted value. Downstream consumers (workbench, mechatron graph, assembly dashboard) read meta_hint from the command descriptor and apply the v1.1 namespace helpers to annotate the resulting solid.


@ui(...) — Workbench Widget Hints

Syntax: placed after the type annotation and before the default value.

command MY_PART(
    radius:    float @ui(widget="circle_r", label="Radius", snap="mm") = 50.0,
    n_sides:   int   @ui(label="Sides", min=3, max=64)                 = 6,
    label:     string @ui(label="Name", group="Metadata")              = "part",
    thickness: float = 3.0   # no @ui — plain parameter, no widget hint
) -> solid:
    ...

Recognised keys

Key

Type

Description

widget

string

Widget type. Currently defined: "circle_r" (drag-handle circle radius), "slider" (linear slider).

label

string

Human-readable label shown in the parameter panel.

snap

string

Value-snap preset. Defined presets: "mm", "metric_tap", "unified_clearance".

min

float/int

Minimum allowed value (advisory; not enforced by evaluator).

max

float/int

Maximum allowed value (advisory; not enforced by evaluator).

step

float

Slider step size.

group

string

UI group/section heading for the parameter panel.

All key values must be literals (strings, numbers, booleans, or lists of literals). Non-literal expressions are accepted by the parser but stringified.

Surfaced through the service API

The /dsl/commands REST endpoint includes ui_hint in each parameter object when a @ui decorator is present:

{
  "name": "radius",
  "type": "float",
  "default": 50.0,
  "ui_hint": { "widget": "circle_r", "label": "Radius", "snap": "mm" }
}

The /dsl/ui_eval endpoint (POST) evaluates a command and returns its scalar or list-of-scalars result — useful for commands that compute a derived display value from current widget state rather than emitting geometry.

Types

Primitive Types

Type

Description

Examples

int

Integer number

42, -10, 0

float

Floating-point number

3.14, -1.5, 0.0

bool

Boolean

true, false

string

Text string

"hello", "gear_1"

Geometric Types

Type

Description

Constructor

point

2D or 3D point

point(x, y) or point(x, y, z)

point2d

2D point

point2d(x, y)

point3d

3D point

point(x, y, z)

vector

2D or 3D direction

vector(dx, dy) or vector(dx, dy, dz)

vector2d

2D vector

vector2d(dx, dy)

vector3d

3D vector

vector(dx, dy, dz)

transform

Transformation matrix

translate_xform(), rotate_xform(), etc.

Curve Types

Type

Description

Constructor

line_segment

Straight line

line(start, end)

arc

Circular arc

arc(center, radius, start_angle, end_angle)

circle

Full circle

circle(center, radius)

ellipse

Ellipse or elliptical arc

ellipse(center, semi_major, semi_minor, ...)

bezier

Bezier curve

bezier(control_points)

catmullrom

Catmull-Rom spline

catmullrom(points, closed?, alpha?)

nurbs

NURBS curve

nurbs(points, weights?, degree?)

Compound Types

Type

Description

Constructor

path2d

2D path of segments

make_path2d(curves)

path3d

3D path of segments

make_path3d(segments), path3d_line(), path3d_arc()

region2d

Closed 2D region

rectangle(), regular_polygon(), polygon(), disk()

solid

3D solid volume

box(), cylinder(), sphere(), etc.

Generic Types

Type

Description

Example

list<T>

List of elements

list<float>, list<point>, list<solid>

Built-in Functions

Math Functions

Trig functions use RADIANS (sin, cos, tan take radians; asin, acos, atan, atan2 return radians). This is the opposite of the geometry/transform functions (rotate, arc, ellipse), which use degrees. Use radians(deg) / degrees(rad) to convert between them.

# Trigonometry (argument in radians)
sin(x: float) -> float
cos(x: float) -> float
tan(x: float) -> float
asin(x: float) -> float
acos(x: float) -> float
atan(x: float) -> float
atan2(y: float, x: float) -> float

# General math
sqrt(x: float) -> float
abs(x: float) -> float
pow(base: float, exp: float) -> float
exp(x: float) -> float          # e^x
log(x: float) -> float          # natural logarithm
log10(x: float) -> float        # base-10 logarithm
floor(x: float) -> int
ceil(x: float) -> int
round(x: float) -> int
min(a: float, b: float, ...) -> float  # variadic
max(a: float, b: float, ...) -> float  # variadic

# Angle conversion
radians(degrees: float) -> float
degrees(radians: float) -> float

# Constants
pi() -> float   # 3.14159...
tau() -> float  # 2 * pi

Point and Vector Constructors

# Points
point(x: float, y: float) -> point2d
point(x: float, y: float, z: float) -> point3d
point2d(x: float, y: float) -> point2d

# Vectors
vector(dx: float, dy: float) -> vector2d
vector(dx: float, dy: float, dz: float) -> vector3d
vector2d(dx: float, dy: float) -> vector2d

2D Shape Constructors

# Rectangle centered at a point (or origin if center omitted)
rectangle(width: float, height: float) -> region2d
rectangle(width: float, height: float, center: point2d) -> region2d

# Regular polygon
regular_polygon(sides: int, radius: float) -> region2d
regular_polygon(sides: int, radius: float, center: point2d) -> region2d

# Polygon from list of points
polygon(points: list<point>) -> region2d

# Disk (filled circle as polygon approximation)
disk(center: point, radius: float) -> region2d
disk(center: point, radius: float, segments: int) -> region2d  # default 64 segments

Curve Constructors

# Line segment between two points
line(start: point, end: point) -> line_segment

# Arc from center point (angles in degrees)
arc(center: point, radius: float, start_angle: float, end_angle: float) -> arc

# Full circle
circle(center: point, radius: float) -> circle

# Ellipse (angles in degrees, rotation in degrees)
ellipse(center: point, semi_major: float, semi_minor: float) -> ellipse
ellipse(center: point, semi_major: float, semi_minor: float,
        rotation: float, start: float, end: float) -> ellipse

# Bezier curve from control points
bezier(control_points: list<point>) -> bezier

# Catmull-Rom spline
catmullrom(points: list<point>) -> catmullrom
catmullrom(points: list<point>, closed: bool, alpha: float) -> catmullrom
# alpha: 0.0=uniform, 0.5=centripetal (default), 1.0=chordal

# NURBS curve
nurbs(points: list<point>) -> nurbs
nurbs(points: list<point>, weights: list<float>, degree: int) -> nurbs
# degree default: 3

Curve Sampling Functions

# Sample a point on a curve at parameter t in [0, 1]
sample_curve(curve, t: float) -> point

# Sample n points along a curve
sample_curve_n(curve, n: int) -> list<point>

# Get the length of a curve
curve_length(curve) -> float

Path Constructors

2D Paths

# Create a path from a list of curves
make_path2d(curves: list<curve>) -> path2d

# Close an open path to create a region
close_path(path: path2d) -> region2d

# Convert a spline to a region (polygon approximation)
region_from_spline(spline, segments: int = 64) -> region2d

3D Paths (for sweep operations)

# Create a path3d from segments
make_path3d(segments...) -> path3d  # variadic

# Line segment for path3d
path3d_line(start: point3d, end: point3d) -> path3d

# Arc segment for path3d (explicit normal)
path3d_arc(center: point3d, start: point3d, end: point3d, normal: vector3d) -> path3d

# Arc segment with auto-computed normal
# flip=false: shorter arc, flip=true: longer arc (opposite direction)
path3d_arc_auto(center: point3d, start: point3d, end: point3d, flip: bool) -> path3d

2D Boolean Operations

# Union of two 2D regions
union2d(a: region2d, b: region2d) -> region2d

# Difference (subtract b from a)
difference2d(a: region2d, b: region2d) -> region2d

# Intersection (keep overlapping area)
intersection2d(a: region2d, b: region2d) -> region2d

# Aggregation operations (for lists)
union2d_all(regions: list<region2d>) -> region2d        # Union all regions
difference2d_all(base: region2d, tools: list<region2d>) -> region2d  # Subtract all tools from base
intersection2d_all(regions: list<region2d>) -> region2d  # Intersect all regions

Solid Constructors

# Box: width (X), depth (Y), height (Z) - centered at origin
box(width: float, depth: float, height: float) -> solid

# Cylinder: base at Z=0, extends to Z=height (NOT centered)
# Note: To center, use translate(cylinder(r, h), 0.0, 0.0, -h/2.0)
cylinder(radius: float, height: float) -> solid

# Sphere: centered at origin
sphere(radius: float) -> solid

# Oblate spheroid (flattened sphere like Earth/Mars)
oblate_spheroid(equatorial_diameter: float, oblateness: float) -> solid
# oblateness: 0=sphere, typical values: Earth~0.00335, Mars~0.00648

# Cone/frustum: radius1 at bottom, radius2 at top
# Base at Z=0, extends to Z=height (NOT centered, same as cylinder)
cone(radius1: float, radius2: float, height: float) -> solid

# Involute spur gear (centered at origin, extends Z=0 to face_width)
involute_gear(teeth: int, module_mm: float, pressure_angle: float, face_width: float) -> solid

# === Fasteners (catalog-based with parametric threads) ===

# Metric hex bolt per ISO 4014/4017 (head up, threads at bottom)
# Size examples: "M3", "M4", "M5", "M6", "M8", "M10", "M12", "M14", "M16", "M20", "M24"
metric_hex_bolt(size: string, length: float) -> solid

# Metric hex nut per ISO 4032
metric_hex_nut(size: string) -> solid

# Unified (UNC) hex bolt per ASME B18.2.1
# Size examples: "#4-40", "#6-32", "#8-32", "#10-24", "#12-24",
#                "1/4-20", "5/16-18", "3/8-16", "1/2-13", "5/8-11", "3/4-10", "1-8"
# Length is in inches (converted to mm internally)
unified_hex_bolt(size: string, length: float) -> solid

# Unified hex nut per ASME B18.2.2
unified_hex_nut(size: string) -> solid

Primitive Positioning Summary:

Primitive

X/Y Centering

Z Positioning

box

Centered

Centered

sphere

Centered

Centered

cylinder

Centered

Base at Z=0

cone

Centered

Base at Z=0

involute_gear

Centered

Base at Z=0

metric_hex_bolt

Centered

Head up, tip at Z=0

metric_hex_nut

Centered

Base at Z=0

unified_hex_bolt

Centered

Head up, tip at Z=0

unified_hex_nut

Centered

Base at Z=0

For hollow tubes and more control over positioning, use the Python API directly (yapcad.geom3d_util.tube, conic_tube, etc.).

Solid from 2D Operations

# Extrude a 2D region along Z axis
extrude(profile: region2d, height: float) -> solid

# Revolve a 2D region around an axis
revolve(profile: region2d, axis: vector3d, angle: float) -> solid

# Sweep a 2D profile along a 3D path
sweep(profile: region2d, spine: path3d) -> solid

# Sweep with inner void (hollow tube)
sweep_hollow(outer_profile: region2d, inner_profile: region2d, spine: path3d) -> solid

# Adaptive sweep - profile rotates to track path tangent
# Uses minimal-twist frame (default) to avoid unwanted rotation
sweep_adaptive(profile: region2d, spine: path3d, threshold_deg: float) -> solid

# Adaptive sweep with hollow profile
sweep_adaptive_hollow(
    outer_profile: region2d,
    inner_profile: region2d,
    spine: path3d,
    threshold_deg: float
) -> solid

# Frenet frame variants - profile follows natural curve curvature
# Appropriate for paths like helices where you want natural twisting
sweep_adaptive_frenet(profile: region2d, spine: path3d, threshold_deg: float) -> solid

sweep_adaptive_hollow_frenet(
    outer_profile: region2d,
    inner_profile: region2d,
    spine: path3d,
    threshold_deg: float
) -> solid

# Loft between multiple profiles
loft(profiles: list<region2d>) -> solid

Boolean Operations

# Union (combine) solids
union(a: solid, b: solid) -> solid
union(solids: list<solid>) -> solid  # variadic

# Difference (subtract b from a)
difference(a: solid, b: solid) -> solid
difference(a: solid, tools: list<solid>) -> solid  # variadic

# Intersection (keep overlapping volume)
intersection(a: solid, b: solid) -> solid
intersection(solids: list<solid>) -> solid  # variadic

# Compound - combine without merging (for multi-body assemblies)
compound(a: solid, b: solid) -> solid
compound(solids: list<solid>) -> solid  # variadic

# Aggregation operations (for lists) - cleaner syntax than variadic
union_all(solids: list<solid>) -> solid        # Union all solids in list
difference_all(base: solid, tools: list<solid>) -> solid  # Subtract all tools from base
intersection_all(solids: list<solid>) -> solid  # Intersect all solids in list

Transformation Functions

Angles are in DEGREES. All rotation transforms (rotate, rotate_2d, rotate_xform) and the angle arguments of arc/ellipse take degrees. This is the opposite of the trig math functions (sin/cos/tan), which take radians — convert with radians(...) / degrees(...). Mixing the two type-checks but yields wrong geometry. See the Math Functions section.

# Transform solids directly (angles in degrees)
translate(s: solid, x: float, y: float, z: float) -> solid
rotate(s: solid, rx: float, ry: float, rz: float) -> solid  # Euler angles
scale(s: solid, sx: float, sy: float, sz: float) -> solid

# Create transform matrices (for advanced use)
translate_xform(v: vector) -> transform
rotate_xform(axis: vector3d, angle: float) -> transform
rotate_2d(angle: float) -> transform
scale_xform(factors: vector) -> transform
scale_uniform(factor: float) -> transform
mirror(plane_normal: vector3d) -> transform
mirror_2d(axis: vector2d) -> transform
mirror_y() -> transform  # Convenience: mirror across Y axis
identity_transform() -> transform

# Apply transform to geometry
apply(t: transform, shape: solid) -> solid
apply_surface(t: transform, surf: surface) -> surface
apply_point(t: transform, p: point) -> point
apply_vector(t: transform, v: vector3d) -> vector3d

Query Functions

# Solid queries
volume(s: solid) -> float
surface_area(s: solid) -> float
centroid(s: solid) -> point3d
is_empty(s: solid) -> bool

# Region2D queries
area(r: region2d) -> float
perimeter(r: region2d) -> float

# Distance between points
distance(a: point, b: point, tolerance: float) -> float

List Functions

# List operations
len(lst: list<T>) -> int
range(end: int) -> list<int>                       # [0, 1, ..., end-1]
range(start: int, end: int) -> list<int>           # [start, ..., end-1]
range(start: int, end: int, step: int) -> list<int>
concat(list1: list<T>, list2: list<T>) -> list<T>
reverse(lst: list<T>) -> list<T>
flatten(nested: list<list<T>>) -> list<T>

Aggregation Functions

# Numeric aggregation
sum(values: list<float>) -> float       # Sum all values
product(values: list<float>) -> float   # Multiply all values
min_of(values: list<float>) -> float    # Find minimum value
max_of(values: list<float>) -> float    # Find maximum value

# Boolean aggregation
any_true(values: list<bool>) -> bool    # True if any element is true
all_true(values: list<bool>) -> bool    # True if all elements are true

Utility Functions

# Debug output
print(value, ...) -> bool  # variadic, returns true

# Empty geometry constructors
empty_solid() -> solid
empty_region() -> region2d

Method Syntax

Some types support method-style calls as an alternative to function calls:

Solid Methods

# These are equivalent:
result = translate(my_solid, 10.0, 0.0, 0.0)
result = my_solid.translate(vector(10.0, 0.0, 0.0))

# Available methods on solids:
solid.union(other: solid) -> solid
solid.difference(other: solid) -> solid
solid.intersection(other: solid) -> solid
solid.translate(v: vector) -> solid
solid.rotate(axis: vector3d, angle: float) -> solid
solid.scale(factors: vector) -> solid
solid.apply(t: transform) -> solid

Region2D Methods

region.union(other: region2d) -> region2d
region.difference(other: region2d) -> region2d
region.intersection(other: region2d) -> region2d

Transform Methods

transform.compose(other: transform) -> transform
transform.inverse() -> transform
transform.translation() -> vector3d
transform.is_rigid() -> bool

Curve Methods

curve.at(t: float) -> point
curve.tangent_at(t: float) -> vector
curve.normal_at(t: float) -> vector
curve.curvature_at(t: float) -> float
curve.length() -> float

Statements

Variable Declaration

# Explicit type annotation (preferred)
width: float = 10.0
gear: solid = involute_gear(24, 2.0, 20.0, 10.0)

# With 'let' keyword (also supported)
let height: float = 5.0

Require (Assertions)

# Validate parameters - raises error if false
require width > 0.0
require height > 0.0 and depth > 0.0

Emit (Output)

# Return the result from a command
emit result

For Loops

# Iterate over a range
for i in range(10):
    # body

# Iterate over a list
for item in my_list:
    # body

Note: The DSL does not support while loops. This restriction ensures all DSL programs are statically verifiable (guaranteed to terminate). Use for i in range(max_iterations) with early return instead:

command FIND_ROOT(start: float) -> float:
    x: float = start
    for i in range(100):  # Maximum 100 iterations
        if abs(f(x)) < 0.001:
            emit x  # Early return when condition met
        x = improve(x)
    emit x  # Return best result after max iterations

Command Recursion

Commands can call other commands, enabling modular designs:

command make_branch(depth: int) -> solid:
    if depth <= 0:
        emit cylinder(1.0, 5.0)
    branch: solid = cylinder(1.0, 5.0)
    sub: solid = make_branch(depth - 1)  # Recursive call
    emit union(branch, translate(sub, 0.0, 0.0, 5.0))

Recursion Limits: To prevent runaway recursion, command-to-command call depth is limited to 100 by default. The type checker warns about recursive call patterns. Configure the limit via:

  • CLI: --recursion-limit 200

  • Environment: YAPCAD_DSL_RECURSION_LIMIT=200

Conditionals

if condition:
    # body
elif other_condition:
    # body
else:
    # body

Conditional Expressions (Ternary)

The DSL supports Python-style conditional expressions for inline value selection:

# Basic syntax: value_if_true if condition else value_if_false
result: float = 10.0 if use_metric else 25.4 * value

# With comparison
status: string = "hot" if temperature > 100.0 else "cold"

# Nested (chained) conditionals
grade: string = "A" if score >= 90.0 else ("B" if score >= 80.0 else "C")

# Selecting geometry
shape: solid = box(10.0, 10.0, 10.0) if use_cube else cylinder(5.0, 10.0)

Conditional expressions are useful for:

  • Selecting between two values based on a boolean parameter

  • Unit conversion (metric vs imperial)

  • Choosing geometry based on configuration

  • Inline computation without separate if/else blocks

Note: Both branches must have compatible types, and the condition must be a boolean.

List Comprehensions

Create lists using comprehension syntax:

# Map: transform each element
squares: list<float> = [x * x for x in values]

# Generate from range
angles: list<float> = [i * 30.0 for i in range(12)]

# Filter: select elements matching condition
positives: list<float> = [x for x in values if x > 0.0]

# Combined map and filter
big_squares: list<float> = [x * x for x in values if x > 10.0]

Nested Comprehensions

Multiple for clauses create nested iterations (cartesian products):

# Nested comprehension - generates all (x, y) combinations
sums: list<int> = [x + y for x in xs for y in ys]
# Equivalent to: for x in xs: for y in ys: append(x + y)

# With conditions on outer loop
filtered_outer: list<int> = [x + y for x in xs if x > 0 for y in ys]

# With conditions on inner loop
filtered_inner: list<int> = [x + y for x in xs for y in ys if y < 10]

# With conditions on both loops
filtered_both: list<int> = [x + y for x in xs if x > 0 for y in ys if y < 10]

# Triple nesting
products: list<int> = [x + y + z for x in xs for y in ys for z in zs]

Resource Limits: To prevent combinatorial explosion, list comprehensions have two limits:

  • Maximum nesting depth: 4 levels (e.g., [... for a in xs for b in ys for c in zs for d in ws])

  • Maximum result size: 100,000 elements

Exceeding either limit raises a runtime error. For larger datasets, use explicit for loops with batch processing.

Example: Creating patterns with symmetric geometry

# Generate holes at 3 sectors (0°, 120°, 240°) with multiple angles per sector
sector_offsets: list<float> = [0.0, 120.0, 240.0]
base_angles: list<float> = [50.0, 60.0, 70.0]

all_holes: list<solid> = [
    make_hole(radius, thickness, base_angle + offset)
    for offset in sector_offsets
    for base_angle in base_angles
]
# Creates 9 holes: 3 sectors × 3 angles per sector

Common Patterns

Box with Hole

module bracket

command MAKE_BRACKET(
    width: float,
    height: float,
    thickness: float,
    hole_radius: float
) -> solid:
    # Create main plate
    plate: solid = box(width, height, thickness)

    # Create hole cylinder (slightly longer for clean cut)
    hole: solid = cylinder(hole_radius, thickness + 1.0)

    # Position hole at center of plate (adjust Z to cut through)
    hole_pos: solid = translate(hole, 0.0, 0.0, -0.5)

    # Subtract hole from plate
    result: solid = difference(plate, hole_pos)
    emit result

Hollow Box (Shell)

module enclosure

command MAKE_ENCLOSURE(
    width: float,
    depth: float,
    height: float,
    wall_thickness: float
) -> solid:
    require wall_thickness < width / 2.0
    require wall_thickness < depth / 2.0

    outer: solid = box(width, depth, height)

    inner_w: float = width - 2.0 * wall_thickness
    inner_d: float = depth - 2.0 * wall_thickness
    inner_h: float = height - wall_thickness

    inner: solid = box(inner_w, inner_d, inner_h)
    inner_positioned: solid = translate(inner, 0.0, 0.0, wall_thickness)

    shell: solid = difference(outer, inner_positioned)
    emit shell

Swept Path with Bends

module pipe_jig

# Helper: create a path with two bends
command make_bent_path(
    seg1_length: float,
    seg2_length: float,
    seg3_length: float,
    bend_angle: float
) -> path3d:
    angle_rad: float = radians(bend_angle)
    double_rad: float = radians(2.0 * bend_angle)

    # First segment along Y
    p0: point3d = point(0.0, 0.0, 0.0)
    p1: point3d = point(0.0, seg1_length, 0.0)

    # Direction after first bend
    dir1_x: float = sin(angle_rad)
    dir1_y: float = cos(angle_rad)

    # Second bend point
    p2_x: float = seg2_length * dir1_x
    p2_y: float = seg1_length + seg2_length * dir1_y
    p2: point3d = point(p2_x, p2_y, 0.0)

    # Direction after second bend
    dir2_x: float = sin(double_rad)
    dir2_y: float = cos(double_rad)

    # End point
    p3_x: float = p2_x + seg3_length * dir2_x
    p3_y: float = p2_y + seg3_length * dir2_y
    p3: point3d = point(p3_x, p3_y, 0.0)

    # Build path segments
    seg1: path3d = path3d_line(p0, p1)
    seg2: path3d = path3d_line(p1, p2)
    seg3: path3d = path3d_line(p2, p3)

    spine: path3d = make_path3d(seg1, seg2, seg3)
    emit spine

# Main command: sweep a profile along the bent path
command MAKE_BENT_TUBE(
    profile_width: float = 10.0,
    profile_height: float = 10.0,
    bend_angle: float = 15.0
) -> solid:
    # Create profile
    profile: region2d = rectangle(profile_width, profile_height)

    # Create path
    spine: path3d = make_bent_path(50.0, 200.0, 50.0, bend_angle)

    # Sweep with adaptive tangent tracking (5 degree threshold)
    result: solid = sweep_adaptive(profile, spine, 5.0)
    emit result

Gear Creation

module gears

command MAKE_SPUR_GEAR(
    teeth: int,
    module_mm: float,
    face_width: float
) -> solid:
    require teeth >= 6
    require module_mm > 0.0

    # Standard 20-degree pressure angle
    gear: solid = involute_gear(teeth, module_mm, 20.0, face_width)
    emit gear

Pattern with For Loop

module assembly

command MAKE_PEGBOARD(count: int) -> solid:
    require count > 0 and count <= 10

    # Create base
    base: solid = box(100.0, 100.0, 10.0)
    peg: solid = cylinder(5.0, 20.0)

    # Create pegs in a grid
    result: solid = base
    spacing: float = 80.0 / (count + 1)

    for i in range(count):
        for j in range(count):
            x: float = -40.0 + (i + 1) * spacing
            y: float = -40.0 + (j + 1) * spacing
            peg_pos: solid = translate(peg, x, y, 10.0)
            result = union(result, peg_pos)

    emit result

Pattern with Aggregation Functions

Using union_all and difference_all for cleaner multi-body operations:

module thrust_plate

command MAKE_PLATE_WITH_HOLES(
    diameter: float,
    thickness: float,
    hole_radius: float
) -> solid:
    # Create base plate
    plate: solid = cylinder(diameter / 2.0, thickness)

    # Create holes at 120° intervals using list comprehension
    hole_angles: list<float> = [i * 120.0 for i in range(3)]
    holes: list<solid> = [
        translate(
            cylinder(hole_radius, thickness + 2.0),
            (diameter / 3.0) * cos(radians(angle)),
            (diameter / 3.0) * sin(radians(angle)),
            -1.0
        )
        for angle in hole_angles
    ]

    # Subtract all holes at once using difference_all
    result: solid = difference_all(plate, holes)
    emit result

Using union_all to combine multiple parts:

module assembly

command MAKE_FRAME() -> solid:
    # Create individual parts
    bottom_rail: solid = box(100.0, 10.0, 10.0)
    top_rail: solid = translate(box(100.0, 10.0, 10.0), 0.0, 0.0, 50.0)
    left_post: solid = translate(box(10.0, 10.0, 50.0), -45.0, 0.0, 25.0)
    right_post: solid = translate(box(10.0, 10.0, 50.0), 45.0, 0.0, 25.0)

    # Union all parts at once
    parts: list<solid> = [bottom_rail, top_rail, left_post, right_post]
    frame: solid = union_all(parts)
    emit frame

Spline-Based Profile

module organic

command MAKE_BLOB(scale: float = 10.0) -> solid:
    # Create organic shape using Catmull-Rom spline
    pts: list<point> = [
        point(1.0 * scale, 0.0),
        point(0.8 * scale, 0.6 * scale),
        point(0.0, 1.0 * scale),
        point(-0.8 * scale, 0.6 * scale),
        point(-1.0 * scale, 0.0),
        point(-0.8 * scale, -0.6 * scale),
        point(0.0, -1.0 * scale),
        point(0.8 * scale, -0.6 * scale)
    ]

    # Create closed Catmull-Rom spline
    spline: catmullrom = catmullrom(pts, true, 0.5)

    # Convert to region and extrude
    profile: region2d = region_from_spline(spline, 64)
    result: solid = extrude(profile, scale * 0.5)
    emit result

Type System Notes

  1. Int/Float compatibility: int is assignable to float parameters

  2. Point polymorphism: point2d and point3d are subtypes of point

  3. Vector polymorphism: vector2d and vector3d are subtypes of vector

  4. Explicit typing required: All variables must have explicit type annotations

Error Messages

The DSL provides detailed error messages with source locations:

error[E201]: Type mismatch: expected float, got string
  --> design.dsl:5:12
   |
 5 |     x: float = "hello"
   |            ^^^^^

Common error codes:

  • E101: Parser errors (syntax)

  • E201: Type errors

  • E301: Undefined variables/functions

  • E302: Duplicate definitions

Package Integration

When using --package, the DSL automatically:

  • Creates a .ycpkg directory structure

  • Exports geometry to JSON format

  • Generates STEP export

  • Records provenance metadata (DSL command, parameters, version)

# Create a package with full provenance
python -m yapcad.dsl run design.dsl MAKE_PART --package output.ycpkg

# View the package
python tools/ycpkg_viewer.py output.ycpkg

Programmatic API

For scripts and automation:

from yapcad.dsl import compile_and_run

# Run a DSL command programmatically
source = open("design.dsl").read()
result = compile_and_run(source, "MAKE_PART", {"width": 10.0, "height": 5.0})

if result.success:
    solid = result.emit_result.data  # The yapCAD solid
    # Use the solid...
else:
    print(f"Error: {result.error_message}")

Static Verifiability and Safety

The yapCAD DSL is designed for static verifiability: programs are guaranteed to terminate and have bounded resource consumption (within configured limits). This makes DSL files safe to execute without manual review, unlike arbitrary Python code.

Safety Guarantees

Feature

Guarantee

Mechanism

Iteration

Bounded

for loops iterate over pre-computed finite lists; while is not supported

Recursion

Depth-limited

Command-to-command calls limited to 100 (configurable)

List sizes

Bounded

Comprehensions limited to 100,000 elements

Nesting

Depth-limited

Comprehensions limited to 4 nesting levels

Resource Limits

Resource

Default Limit

Configuration

Recursion depth

100

--recursion-limit N or YAPCAD_DSL_RECURSION_LIMIT

Comprehension elements

100,000

Code constant (recompile to change)

Comprehension nesting

4 levels

Code constant (recompile to change)

Escape Hatches (Advanced Use)

For complex operations that cannot be expressed in pure DSL, two escape hatches exist. These bypass static verifiability and require manual review:

@meta decorator (on commands) and @ui decorator (on parameters) — see Parameter Decorators above.

@native decorator - Embed Python functions:

@native
def complex_calculation(x: float, y: float) -> float:
    # Arbitrary Python code - not statically verified
    import numpy as np
    return float(np.sqrt(x**2 + y**2))

Legacy Python blocks (deprecated):

native python {
    def helper(x):
        return x * 2
} exports {
    helper(x: float) -> float
}

The type checker issues warnings (W212, W213) when native code is present, indicating that manual review is required for type safety.

Type Checker Warnings

Code

Meaning

W212

Native Python block requires manual review

W213

Native function requires manual review

W301

Direct recursion detected (command calls itself)

W302

Mutual recursion detected (command call cycle)

See Also

  • docs/yapCADone.rst - yapCAD 1.0 roadmap and feature status

  • docs/ycpkg_spec.rst - Package specification

  • examples/ - Example DSL files