# yapCAD DSL Reference > **Looking for how to *write* DSL, not just look up a function?** Start with the > [DSL Language Guide](dsl_guide.md) (mental model, idioms) and the > [DSL Tutorial](dsl_tutorial.md) (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 ```python module my_design command MAKE_BOX(width: float, height: float, depth: float) -> solid: result: solid = box(width, height, depth) emit result ``` ## CLI Usage ```bash # 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 ```python 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. ```python @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: ```json { "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. ```python 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: ```json { "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` | List of elements | `list`, `list`, `list` | ## 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. ```python # 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 ```python # 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 ```python # 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) -> 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 ```python # 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) -> bezier # Catmull-Rom spline catmullrom(points: list) -> catmullrom catmullrom(points: list, closed: bool, alpha: float) -> catmullrom # alpha: 0.0=uniform, 0.5=centripetal (default), 1.0=chordal # NURBS curve nurbs(points: list) -> nurbs nurbs(points: list, weights: list, degree: int) -> nurbs # degree default: 3 ``` ### Curve Sampling Functions ```python # 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 # Get the length of a curve curve_length(curve) -> float ``` ### Path Constructors #### 2D Paths ```python # Create a path from a list of curves make_path2d(curves: list) -> 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) ```python # 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 ```python # 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 # Union all regions difference2d_all(base: region2d, tools: list) -> region2d # Subtract all tools from base intersection2d_all(regions: list) -> region2d # Intersect all regions ``` ### Solid Constructors ```python # 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 ```python # 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) -> solid ``` ### Boolean Operations ```python # Union (combine) solids union(a: solid, b: solid) -> solid union(solids: list) -> solid # variadic # Difference (subtract b from a) difference(a: solid, b: solid) -> solid difference(a: solid, tools: list) -> solid # variadic # Intersection (keep overlapping volume) intersection(a: solid, b: solid) -> solid intersection(solids: list) -> solid # variadic # Compound - combine without merging (for multi-body assemblies) compound(a: solid, b: solid) -> solid compound(solids: list) -> solid # variadic # Aggregation operations (for lists) - cleaner syntax than variadic union_all(solids: list) -> solid # Union all solids in list difference_all(base: solid, tools: list) -> solid # Subtract all tools from base intersection_all(solids: list) -> 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. ```python # 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 ```python # 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 ```python # List operations len(lst: list) -> int range(end: int) -> list # [0, 1, ..., end-1] range(start: int, end: int) -> list # [start, ..., end-1] range(start: int, end: int, step: int) -> list concat(list1: list, list2: list) -> list reverse(lst: list) -> list flatten(nested: list>) -> list ``` ### Aggregation Functions ```python # Numeric aggregation sum(values: list) -> float # Sum all values product(values: list) -> float # Multiply all values min_of(values: list) -> float # Find minimum value max_of(values: list) -> float # Find maximum value # Boolean aggregation any_true(values: list) -> bool # True if any element is true all_true(values: list) -> bool # True if all elements are true ``` ### Utility Functions ```python # 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 ```python # 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 ```python region.union(other: region2d) -> region2d region.difference(other: region2d) -> region2d region.intersection(other: region2d) -> region2d ``` ### Transform Methods ```python transform.compose(other: transform) -> transform transform.inverse() -> transform transform.translation() -> vector3d transform.is_rigid() -> bool ``` ### Curve Methods ```python 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 ```python # 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) ```python # Validate parameters - raises error if false require width > 0.0 require height > 0.0 and depth > 0.0 ``` ### Emit (Output) ```python # Return the result from a command emit result ``` ### For Loops ```python # 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: ```python 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: ```python 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 ```python if condition: # body elif other_condition: # body else: # body ``` ### Conditional Expressions (Ternary) The DSL supports Python-style conditional expressions for inline value selection: ```python # 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: ```python # Map: transform each element squares: list = [x * x for x in values] # Generate from range angles: list = [i * 30.0 for i in range(12)] # Filter: select elements matching condition positives: list = [x for x in values if x > 0.0] # Combined map and filter big_squares: list = [x * x for x in values if x > 10.0] ``` #### Nested Comprehensions Multiple `for` clauses create nested iterations (cartesian products): ```python # Nested comprehension - generates all (x, y) combinations sums: list = [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 = [x + y for x in xs if x > 0 for y in ys] # With conditions on inner loop filtered_inner: list = [x + y for x in xs for y in ys if y < 10] # With conditions on both loops filtered_both: list = [x + y for x in xs if x > 0 for y in ys if y < 10] # Triple nesting products: list = [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** ```python # Generate holes at 3 sectors (0°, 120°, 240°) with multiple angles per sector sector_offsets: list = [0.0, 120.0, 240.0] base_angles: list = [50.0, 60.0, 70.0] all_holes: list = [ 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 ```python 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) ```python 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 ```python 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 ```python 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 ```python 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: ```python 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 = [i * 120.0 for i in range(3)] holes: list = [ 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: ```python 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 = [bottom_rail, top_rail, left_post, right_post] frame: solid = union_all(parts) emit frame ``` ### Spline-Based Profile ```python module organic command MAKE_BLOB(scale: float = 10.0) -> solid: # Create organic shape using Catmull-Rom spline pts: list = [ 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) ```bash # 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: ```python 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](#parameter-decorators) above. **@native decorator** - Embed Python functions: ```python @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): ```python 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