submilli-engine 0.3.0

Compiles Submilli programs, a TypeScript subset, to WebAssembly and runs them under capability policy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
//! The `submilli:http` package declaration — the LLM-facing type surface
//! (`Response`, `DownloadOptions`, `DownloadResult`, `Headers`, verb helpers).

use std::collections::BTreeMap;

use crate::{
    DefaultValue, Dispatch, MethodSig, PackageDeclaration, Param, PropertySig, Span, Type,
    TypeKind, TypeSymbol, ValueKind, ValueSymbol,
};

use super::MODULE_NAME;

pub fn package_declaration() -> PackageDeclaration {
    let mut defs = PackageDeclaration::with_package(MODULE_NAME);

    defs.types.insert(
        "Headers".to_string(),
        TypeSymbol {
            name: "Headers".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: TypeKind::Alias {
                generics: Vec::new(),
                ty: Type::InterfaceRef { mangled: crate::mangle::prelude("Map"),
                    package: crate::Package::prelude(),
                    name: "Map".to_string(),
                    args: vec![Type::String, Type::String],
                },
                doc: crate::doc(crate::FileId::HTTP,
                    "/** HTTP header bag: `Map<string, string>` with names lowercased on the wire. Future slice may add case-insensitive lookup. */",
                ),
            },
        },
    );

    insert_response_interface(&mut defs);
    insert_download_options_interface(&mut defs);
    insert_download_result_interface(&mut defs);

    // body-less: prevents get(url, h) from ambiguating body vs headers
    for verb in &["get", "delete", "head", "options"] {
        insert_verb_fn(&mut defs, verb, /* has_body = */ false);
    }
    for verb in &["post", "put", "patch"] {
        insert_verb_fn(&mut defs, verb, /* has_body = */ true);
    }

    defs.values.insert(
        "download".to_string(),
        ValueSymbol {
            name: "download".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "download"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: ValueKind::Function {
                generics: Vec::new(),
                params: vec![
                    Param::new("url", Type::String),
                    Param::new("path", Type::String),
                    Param::with_default(
                        "options",
                        optional_download_options_type(),
                        DefaultValue::Undefined,
                    ),
                ],
                ret: download_result_type(),
                type_predicate: None,
                doc: crate::doc(crate::FileId::HTTP,
                    "/**\n * Download a remote file directly to the VFS — wget-style. Streams the response body to disk so memory stays bounded regardless of file size. Atomic: a crash mid-download leaves a `.tmp` sibling rather than a half-written final file. Traps on transport failure (DNS, connect, TLS, timeout), oversize response, refuse-on-exists, or VFS path escape.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param path Destination VFS path (e.g. `\"/workspace/dataset.csv\"`). The parent directory must exist — call `fs.mkdir` first if needed.\n * @param options Optional `DownloadOptions` bag: `overwrite` (default `false`), `maxBytes` (default and upper bound: tier-configured), `headers`, `timeout` (default and upper bound: 60_000 ms, or a lower operator limit), `decompress` (default `false`). Omit or pass `undefined` to take every default.\n * @capability http.download { host: $url.host, url_path: $url.path, vfs_path: $path, max_bytes: number, overwrite: boolean, decompress: boolean }\n * @capability fs.write { path, max_bytes: number }\n */",
                ),
            },
        },
    );

    defs.values.insert(
        "request".to_string(),
        ValueSymbol {
            name: "request".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "request"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: ValueKind::Function {
                generics: Vec::new(),
                params: vec![
                    Param::new("method", Type::String),
                    Param::new("url", Type::String),
                    Param::with_default(
                        "body",
                        optional_body_type(),
                        DefaultValue::Undefined,
                    ),
                    Param::with_default(
                        "headers",
                        optional_headers_type(),
                        DefaultValue::Undefined,
                    ),
                ],
                ret: response_type(),
                type_predicate: None,
                doc: crate::doc(crate::FileId::HTTP,
                    "/**\n * Issue an HTTP request with a runtime-chosen verb. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param method HTTP method (case-insensitive). Common verbs: `\"GET\"` / `\"POST\"` / `\"PUT\"` / `\"PATCH\"` / `\"DELETE\"` / `\"HEAD\"` / `\"OPTIONS\"`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n * @capability http.get { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.post { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.put { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.patch { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.delete { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.head { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.options { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n */",
                ),
            },
        },
    );

    defs
}

fn insert_verb_fn(defs: &mut PackageDeclaration, verb: &str, has_body: bool) {
    let upper = verb.to_uppercase();
    let doc = if has_body {
        format!(
            "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
        )
    } else {
        format!(
            "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
        )
    };
    let capability = format!(
        " * @capability http.{verb} {{ host: $url.host, path: $url.path, body_size: number, timeout_ms: number }}\n */"
    );
    let doc = doc.replace(" */", &capability);
    let mut params = vec![Param::new("url", Type::String)];
    if has_body {
        params.push(Param::with_default(
            "body",
            optional_body_type(),
            DefaultValue::Undefined,
        ));
    }
    params.push(Param::with_default(
        "headers",
        optional_headers_type(),
        DefaultValue::Undefined,
    ));
    defs.values.insert(
        verb.to_string(),
        ValueSymbol {
            name: verb.to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, verb),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: ValueKind::Function {
                generics: Vec::new(),
                params,
                ret: response_type(),
                type_predicate: None,
                doc: crate::doc(crate::FileId::HTTP, &doc),
            },
        },
    );
}

fn optional_headers_type() -> Type {
    Type::Union(vec![headers_type(), Type::Undefined])
}

fn optional_body_type() -> Type {
    Type::Union(vec![
        Type::String,
        Type::Uint8Array,
        // An empty object is the top object type — any structural object is
        // assignable to it by width subtyping, so this accepts arbitrary object
        // bodies (JSON-encoded at runtime via the `$Object` `toJson` vtable slot).
        Type::Object {
            index: None,
            fields: BTreeMap::new(),
        },
        Type::Array(Box::new(Type::Unknown)),
        Type::Null,
        Type::Undefined,
    ])
}

fn response_type() -> Type {
    Type::InterfaceRef {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "Response"),
        package: crate::Package(MODULE_NAME.to_string()),
        name: "Response".to_string(),
        args: Vec::new(),
    }
}

fn download_result_type() -> Type {
    Type::InterfaceRef {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
        package: crate::Package(MODULE_NAME.to_string()),
        name: "DownloadResult".to_string(),
        args: Vec::new(),
    }
}

fn download_options_type() -> Type {
    Type::InterfaceRef {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
        package: crate::Package(MODULE_NAME.to_string()),
        name: "DownloadOptions".to_string(),
        args: Vec::new(),
    }
}

fn optional_download_options_type() -> Type {
    Type::Union(vec![download_options_type(), Type::Undefined])
}

fn headers_type() -> Type {
    Type::Alias {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
        package: crate::Package(MODULE_NAME.to_string()),
        name: "Headers".to_string(),
        args: Vec::new(),
        ty: Box::new(Type::InterfaceRef {
            mangled: crate::mangle::prelude("Map"),
            package: crate::Package::prelude(),
            name: "Map".to_string(),
            args: vec![Type::String, Type::String],
        }),
    }
}

fn insert_response_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_property(
        &mut properties,
        "body",
        Type::String,
        "/** Response body, decoded as UTF-8. For binary responses, a future `bytes()` method will return `Uint8Array` without lossy decoding. */",
    );
    insert_property(
        &mut properties,
        "headers",
        headers_type(),
        "/** Response headers. Keys lowercased on the wire so `headers.get(\"content-type\")` works regardless of how the server cased them. */",
    );
    insert_property(
        &mut properties,
        "ok",
        Type::Boolean,
        "/** `true` iff `status` is in the 2xx range. */",
    );
    insert_property(
        &mut properties,
        "status",
        Type::Number,
        "/** HTTP status code (e.g. `200`, `404`). */",
    );
    insert_property(
        &mut properties,
        "statusText",
        Type::String,
        "/** Reason phrase (e.g. `\"OK\"`, `\"Not Found\"`). Empty when the server doesn't supply one. */",
    );
    insert_property(
        &mut properties,
        "url",
        Type::String,
        "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
    );

    let mut methods = BTreeMap::new();
    methods.insert(
        "throwForStatus".to_string(),
        MethodSig {
            optional: false,
            generics: Vec::new(),
            params: Vec::new(),
            ret: Type::Void,
            predicate: None,
            doc: crate::doc(crate::FileId::HTTP,
                "/** Throw if `ok` is `false`. The error message includes the status code, reason phrase, and final URL; catch it with `try`/`catch` like any other `Error`. */",
            ),
        },
    );
    methods.insert(
        "toString".to_string(),
        MethodSig {
            optional: false,
            generics: Vec::new(),
            params: Vec::new(),
            ret: Type::String,
            predicate: None,
            doc: crate::doc(crate::FileId::HTTP,
                "/** Concise description of the response — `\"Response(200 OK, https://...)\"`. Built host-side for efficiency. */",
            ),
        },
    );
    // Recognised and lowered by the typechecker to `JSON.parse(body)`; it has no host
    // export. Declared here only for the LLM-facing type surface — codegen skips
    // emitting its import.
    methods.insert(
        "json".to_string(),
        MethodSig {
            optional: false,
            generics: Vec::new(),
            params: Vec::new(),
            ret: Type::Unknown,
            predicate: None,
            doc: crate::doc(crate::FileId::HTTP,
                "/** Parse the response body as JSON into `unknown`. Validate with `as`, for example `r.json() as User`. */",
            ),
        },
    );

    defs.types.insert(
        "Response".to_string(),
        TypeSymbol {
            name: "Response".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Response"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: TypeKind::Interface { index: None,
                generics: Vec::new(),
                methods,
                properties,
                dispatch: Dispatch::Direct,
                doc: crate::doc(crate::FileId::HTTP,
                    "/** Result of an HTTP call — status, headers, body, and the final URL after any redirects. Constructed only by the verb-form helpers; user code reads fields via property access and calls `throwForStatus()` / `toString()`. */",
                ),
            },
        },
    );
}

fn insert_download_options_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_optional_property(
        &mut properties,
        "overwrite",
        Type::Boolean,
        "/** When `true`, clobber an existing file at `path`. Default `false` — refuse with a trap, since LLMs retry operations and silent overwrites cause data loss. */",
    );
    insert_optional_property(
        &mut properties,
        "maxBytes",
        Type::Number,
        "/** Maximum bytes to write to disk (post-decompression when `decompress` is true). Default: the tier-configured global limit. The download traps with a `response too large` error if exceeded. */",
    );
    insert_optional_property(
        &mut properties,
        "headers",
        optional_headers_type(),
        "/** Request headers — typically used for auth (`Authorization: Bearer …`). */",
    );
    insert_optional_property(
        &mut properties,
        "timeout",
        Type::Number,
        "/** Per-request timeout in milliseconds. Default 60_000 (vs 30_000 for the verb-form helpers — downloads are usually larger). */",
    );
    insert_optional_property(
        &mut properties,
        "decompress",
        Type::Boolean,
        "/** When `true`, transparently gunzip / unzstd the response if the `Content-Encoding` header (or URL suffix `.gz` / `.zst`) advertises a compressed stream. Default `false`: downloading `data.csv.gz` saves the `.gz` file as-is. */",
    );

    defs.types.insert(
        "DownloadOptions".to_string(),
        TypeSymbol {
            name: "DownloadOptions".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: TypeKind::Interface { index: None,
                generics: Vec::new(),
                methods: BTreeMap::new(),
                properties,
                // VTable: fields read via inline field-name scan, no per-field imports emitted.
                dispatch: Dispatch::VTable,
                doc: crate::doc(crate::FileId::HTTP,
                    "/** Options bag for [`download`]. All fields optional. Pass `undefined` (or omit the param) to take every default. */",
                ),
            },
        },
    );
}

fn insert_optional_property(
    properties: &mut BTreeMap<String, PropertySig>,
    name: &str,
    ty: Type,
    doc: &str,
) {
    properties.insert(
        name.to_string(),
        PropertySig {
            ty,
            readonly: false,
            intrinsic: false,
            optional: true,
            doc: crate::doc(crate::FileId::HTTP, doc),
        },
    );
}

fn insert_download_result_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_property(
        &mut properties,
        "bytesWritten",
        Type::Number,
        "/** Bytes actually written to disk (post-decompression when `decompress` was set). */",
    );
    insert_property(
        &mut properties,
        "contentType",
        Type::String,
        "/** `Content-Type` header from the response, lowercased. Empty string when the server didn't send one. */",
    );
    insert_property(
        &mut properties,
        "duration_ms",
        Type::Number,
        "/** Wall-clock duration of the download in milliseconds, measured from temp-file open through atomic rename. */",
    );
    insert_property(
        &mut properties,
        "finalUrl",
        Type::String,
        "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
    );
    insert_property(
        &mut properties,
        "path",
        Type::String,
        "/** The VFS path the file was written to — echoes the `path` argument. */",
    );
    insert_property(
        &mut properties,
        "status",
        Type::Number,
        "/** HTTP status code (e.g. `200`). The download does not trap on non-2xx — a 404 still writes the response body to disk, on the theory that an HTML error page is sometimes what an agent wants. Check `status` to discriminate. */",
    );

    let mut methods = BTreeMap::new();
    methods.insert(
        "toString".to_string(),
        MethodSig {
            optional: false,
            generics: Vec::new(),
            params: Vec::new(),
            ret: Type::String,
            predicate: None,
            doc: crate::doc(crate::FileId::HTTP,
                "/** Concise description — `\"Download(200, 1024 bytes -> /workspace/file.csv)\"`. */",
            ),
        },
    );

    defs.types.insert(
        "DownloadResult".to_string(),
        TypeSymbol {
            name: "DownloadResult".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
            declaration_span: Span::at(crate::FileId::HTTP),
            kind: TypeKind::Interface { index: None,
                generics: Vec::new(),
                methods,
                properties,
                dispatch: Dispatch::Direct,
                doc: crate::doc(crate::FileId::HTTP,
                    "/** Result of [`download`] — what was written, where, and the response metadata. */",
                ),
            },
        },
    );
}

fn insert_property(
    properties: &mut BTreeMap<String, PropertySig>,
    name: &str,
    ty: Type,
    doc: &str,
) {
    properties.insert(
        name.to_string(),
        PropertySig {
            ty,
            readonly: true,
            intrinsic: false,
            optional: false,
            doc: crate::doc(crate::FileId::HTTP, doc),
        },
    );
}