|
| 1 | +# Pact |
| 2 | + |
| 3 | +**Pact** - Package interface specification for Theater. |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Exploratory design document. Capturing ideas from initial exploration. |
| 8 | + |
| 9 | +## Motivation |
| 10 | + |
| 11 | +Theater has different goals and constraints than wasmtime, so it is time to re-evaluate the design decisions that we have inherited from WIT. Wasmtime made many decisions in the name of safety, with the goal of running untrusted code together on the same machine. Fundamentally, their atom is the Component. Theater's atom is the actor, which could be composed of multiple packages. |
| 12 | + |
| 13 | +Pact is Theater's answer to WIT - a type system for describing package interfaces, designed for first-class manipulation by packages themselves. |
| 14 | + |
| 15 | +## Core Primitives |
| 16 | + |
| 17 | +### 1. Types |
| 18 | + |
| 19 | +Primitive types: |
| 20 | +``` |
| 21 | +bool, u8, u16, u32, u64, s8, s16, s32, s64, f32, f64, char, string |
| 22 | +``` |
| 23 | + |
| 24 | +### 2. Type Constructors |
| 25 | + |
| 26 | +Type constructors are functions from types to types: |
| 27 | +``` |
| 28 | +list: Type -> Type |
| 29 | +option: Type -> Type |
| 30 | +result: (Type, Type) -> Type |
| 31 | +``` |
| 32 | + |
| 33 | +### 3. Records |
| 34 | + |
| 35 | +Named product types: |
| 36 | +``` |
| 37 | +record point { |
| 38 | + x: f32, |
| 39 | + y: f32, |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +### 4. Variants |
| 44 | + |
| 45 | +Tagged unions: |
| 46 | +``` |
| 47 | +variant shape { |
| 48 | + circle(f32), |
| 49 | + rectangle(f32, f32), |
| 50 | + point, |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +### 5. Functions |
| 55 | + |
| 56 | +First-class functions with explicit signatures: |
| 57 | +``` |
| 58 | +func(s32, s32) -> s32 |
| 59 | +``` |
| 60 | + |
| 61 | +Functions can be: |
| 62 | +- Passed as values |
| 63 | +- Returned from other functions |
| 64 | +- Stored in records |
| 65 | + |
| 66 | +### 6. Interfaces |
| 67 | + |
| 68 | +Interfaces are first-class values describing the full contract of a package - what it imports and what it exports. |
| 69 | + |
| 70 | +``` |
| 71 | +interface calculator { |
| 72 | + imports { logger, types.big-number } |
| 73 | + exports { |
| 74 | + add: func(big-number, big-number) -> big-number; |
| 75 | + sub: func(big-number, big-number) -> big-number; |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +### 7. Metadata |
| 81 | + |
| 82 | +Interfaces can carry typed metadata using `@` annotations: |
| 83 | + |
| 84 | +``` |
| 85 | +interface calculator { |
| 86 | + @version: string = "1.2.3" |
| 87 | + @author: string = "colin" |
| 88 | + @retry-count: u32 = 3 |
| 89 | + @config: CalculatorConfig = { timeout: 30, debug: false } |
| 90 | +
|
| 91 | + imports { ... } |
| 92 | + exports { ... } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +- Built-in metadata (e.g., `@version`) - Pack understands these |
| 97 | +- User-defined metadata - any `@name: Type = value` |
| 98 | +- Metadata flows with the interface when passed around |
| 99 | +- Packages can inspect metadata: `calculator.@version` |
| 100 | + |
| 101 | +As first-class values, interfaces can be: |
| 102 | +- Passed to functions |
| 103 | +- Returned from functions |
| 104 | +- Stored in records |
| 105 | +- Manipulated programmatically |
| 106 | + |
| 107 | +This moves interface operations into package space - instead of special tooling with hardcoded operations, packages can write whatever interface manipulations they need. |
| 108 | + |
| 109 | +## Syntax |
| 110 | + |
| 111 | +- Comments: `//` |
| 112 | +- Terminators: semicolons (`;`) |
| 113 | +- Blocks: braces (`{ }`) |
| 114 | +- Type annotations: colon (`:`) |
| 115 | +- Metadata: `@name: Type = value;` |
| 116 | + |
| 117 | +## File Structure |
| 118 | + |
| 119 | +Pact files live in a `pact/` directory: |
| 120 | + |
| 121 | +``` |
| 122 | +pact/ |
| 123 | + calculator.pact |
| 124 | + logger.pact |
| 125 | + types.pact |
| 126 | +``` |
| 127 | + |
| 128 | +No special `package` or `world` declarations. Each file defines interfaces. Reference other files with dot notation: |
| 129 | + |
| 130 | +``` |
| 131 | +// In calculator.pact |
| 132 | +interface calculator { |
| 133 | + imports { |
| 134 | + logger.log, // function from logger.pact |
| 135 | + types.BigNum // type from types.pact |
| 136 | + } |
| 137 | + exports { |
| 138 | + add: func(types.BigNum, types.BigNum) -> types.BigNum |
| 139 | + } |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +Namespacing via nested interfaces: |
| 144 | + |
| 145 | +``` |
| 146 | +interface my-org { |
| 147 | + interface calculator { ... } |
| 148 | + interface logger { ... } |
| 149 | +} |
| 150 | +
|
| 151 | +// Access: my-org.calculator.add |
| 152 | +``` |
| 153 | + |
| 154 | +Versioning via nesting or metadata: |
| 155 | + |
| 156 | +``` |
| 157 | +interface calculator { |
| 158 | + @version: string = "2.0.0" |
| 159 | + ... |
| 160 | +} |
| 161 | +
|
| 162 | +// Or nested versions |
| 163 | +interface calculator { |
| 164 | + interface v1 { ... } |
| 165 | + interface v2 { ... } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +## Type Operations |
| 170 | + |
| 171 | +### On Types |
| 172 | + |
| 173 | +Standard type constructors: |
| 174 | +``` |
| 175 | +list<T> // List of T |
| 176 | +option<T> // Optional T |
| 177 | +result<T, E> // Success T or error E |
| 178 | +tuple<T, U, ...> // Product type |
| 179 | +``` |
| 180 | + |
| 181 | +### On Interfaces |
| 182 | + |
| 183 | +Interfaces are data. Write functions that operate on them: |
| 184 | + |
| 185 | +``` |
| 186 | +// Compose two interfaces |
| 187 | +fn compose(a: Interface, b: Interface) -> Interface { |
| 188 | + // Merge imports, merge exports, check for conflicts |
| 189 | +} |
| 190 | +
|
| 191 | +// Check compatibility |
| 192 | +fn satisfies(provider: Interface, consumer: Interface) -> bool { |
| 193 | + // Does provider export what consumer imports? |
| 194 | +} |
| 195 | +
|
| 196 | +// Subset exports |
| 197 | +fn only(i: Interface, funcs: list<string>) -> Interface { |
| 198 | + // Return interface with only specified exports |
| 199 | +} |
| 200 | +
|
| 201 | +// Transform all function signatures |
| 202 | +fn wrap_results(i: Interface) -> Interface { |
| 203 | + // Wrap each export's return type in result<T, error> |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +No blessed operations - packages define whatever manipulations they need. The type system provides the primitives; you build the operations. |
| 208 | + |
| 209 | +## What This Enables |
| 210 | + |
| 211 | +### Programmatic Package Composition |
| 212 | + |
| 213 | +Instead of static manifest files wiring packages together, write code that composes interfaces: |
| 214 | + |
| 215 | +``` |
| 216 | +fn build_system() -> Interface { |
| 217 | + let calc = load_interface("calculator.wasm"); |
| 218 | + let logger = load_interface("logger.wasm"); |
| 219 | +
|
| 220 | + // Check compatibility |
| 221 | + if !satisfies(logger, calc.imports) { |
| 222 | + error("logger doesn't satisfy calculator's imports"); |
| 223 | + } |
| 224 | +
|
| 225 | + // Compose into a system |
| 226 | + compose(calc, logger) |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +### Custom Tooling |
| 231 | + |
| 232 | +Build whatever interface tools you need: |
| 233 | +- Compatibility checkers |
| 234 | +- Binding generators |
| 235 | +- Adapter synthesizers |
| 236 | +- Documentation extractors |
| 237 | + |
| 238 | +These are just packages that operate on interfaces - no special tooling required. |
| 239 | + |
| 240 | +### Runtime Capabilities |
| 241 | + |
| 242 | +Interfaces can also be used at runtime as typed capabilities: |
| 243 | + |
| 244 | +``` |
| 245 | +fn setup() { |
| 246 | + let calc: Calculator = bind(calc_actor_id); |
| 247 | + worker.give_calculator(calc); // Pass capability |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +The same first-class interface serves both compile-time manipulation and runtime capability passing. |
| 252 | + |
| 253 | +## Relationship to Existing Systems |
| 254 | + |
| 255 | +### Pack Compiler |
| 256 | + |
| 257 | +Pack currently has hardcoded interface operations. With first-class interfaces: |
| 258 | +- Pack becomes simpler - it provides primitives, not operations |
| 259 | +- Interface manipulation moves to packages |
| 260 | +- Users can extend/customize without modifying Pack |
| 261 | + |
| 262 | +### Handler Matching |
| 263 | + |
| 264 | +Handlers can be written as packages that operate on interfaces: |
| 265 | +- Inspect an interface's imports |
| 266 | +- Claim interfaces they can satisfy |
| 267 | +- No special handler registration - just interface matching |
| 268 | + |
| 269 | +### RPC |
| 270 | + |
| 271 | +RPC is just one pattern built on first-class interfaces: |
| 272 | +- `bind(actor_id)` returns a capability (interface bound to an actor) |
| 273 | +- Pass capabilities between actors |
| 274 | +- No special RPC mechanism - packages implement whatever patterns they need |
| 275 | + |
| 276 | +## Generics |
| 277 | + |
| 278 | +Type parameters are declared in the interface body with `type`: |
| 279 | + |
| 280 | +``` |
| 281 | +interface storage { |
| 282 | + type T: Serializable; // Type param with constraint |
| 283 | +
|
| 284 | + exports { |
| 285 | + get: func() -> T; |
| 286 | + set: func(T) -> (); |
| 287 | + } |
| 288 | +} |
| 289 | +``` |
| 290 | + |
| 291 | +Constraints use interface names - no separate trait system. `T: Serializable` means T must satisfy the `Serializable` interface. |
| 292 | + |
| 293 | +Instantiation mirrors the body style: |
| 294 | + |
| 295 | +``` |
| 296 | +storage { T = User } |
| 297 | +``` |
| 298 | + |
| 299 | +Multiple type parameters: |
| 300 | + |
| 301 | +``` |
| 302 | +interface pair { |
| 303 | + type A; |
| 304 | + type B; |
| 305 | +} |
| 306 | +
|
| 307 | +pair { A = string, B = u32 } |
| 308 | +``` |
| 309 | + |
| 310 | +## Next Steps |
| 311 | + |
| 312 | +1. [ ] Implement Pact parser in Pack |
| 313 | +2. [ ] Make interfaces introspectable at runtime (first-class) |
| 314 | +3. [ ] Define built-in metadata (`@version`, others?) |
0 commit comments