#[module] lowers each step call in its body to a node in a program graph. Lines that are not step calls, plain Rust like uppercasing a string, cannot be described as steps, so DSRs keeps each one as a hole: a named, typed black box. The graph records the hole’s name, its inputs, and its output type; the interior stays private. A typed border around an unknown interior is still a contract, like a blender: you do not know the circuitry inside, but you know what goes in and what pours out, and that is enough to build the rest of the program around it.
Declaring a hole
A type-ascribedlet becomes a hole. That is the whole mechanism:
drafter and checker are steps. upper is not a step call, so it becomes a hole named upper. Its input is drafter.draft, found automatically, and its output is a String.
Allowed types
The ascribed type becomes a field in the program’s interface, so it must be simple:String- integers (
i64,i32, and friends) - floats (
f64,f32) boolVec<...>andOption<...>of the above
Type anchors
Values that flow into a hole from earlier steps arrive through a decode step at run time, and the compiler sometimes cannot work out their concrete type on its own. The fix is a small innerlet with a type, as an anchor:
let. Calling a method on the incoming value instead, for example ticket.clone(), fails inference, because the method resolves against a type the compiler does not yet know. let t: String = ticket; states the type first, and everything after it infers normally.
Two kinds of holes
The examples below use a hole namedclean that scrubs email addresses out of a ticket string.
Native holes
A hole written in a#[module] body is compiled Rust, and machine code cannot travel in a text artifact. The printed program therefore records only the typed border and a fingerprint of the code, marked extern:
clean: a host binds its native holes by name, and the 16-hex fingerprint lets it verify it holds the same code the file was printed against, not merely something with a matching name. Next to the binary that defined it, the program runs. Loaded anywhere else, it fails immediately with a “host hole unbound” error.
Sandboxed holes
A sandboxed hole carries its implementation inside the file, as JavaScript, executed in a sandbox by whatever host loads it:Inspecting holes
Every module lists its holes in a constant calledOPACITY. For a module frontdesk with the clean hole above:
Forbidding holes
deny_holes makes any line that is not a step call a compile error:
The optimization cost
Holes are opaque to optimizers. An optimizer can rewrite an instruction because an instruction is data with an address; it cannot rewrite the inside of a hole, because there is nothing there to read. Every hole subtracts from the surface an optimizer can improve. Prefer fewer and smaller holes: each line moved out of a hole into a visible step is a line an optimizer can tune and any host can run. Keep the interiors small enough that what stays opaque is genuinely the part with nothing to say.Common mistakes
No type on thelet. A hole must declare its output type. let upper = ...; without a type is a compile error that tells you to write let upper: String = ...;.
Using a whole step result inside a hole. Holes consume output fields, not whole results. Write drafter.draft, not drafter.
Expecting the file to carry the code. It does not. The printed program marks a native hole as extern. Portability stops at the binary that defined it. If you need a fully portable program, replace the native hole with a sandboxed hole in the .dsrs file, or with another step.
See also
- The module macro for the body rules that decide what becomes a step and what becomes a hole
- The .dsrs file for both hole forms in the grammar
- Capabilities for the
caps []printed on every hole - Runtime for binding native holes and the load-time checks
- Optimizers for the tuning surface that holes subtract from
