submilli-engine 0.6.1

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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
//! Type surface of `submilli:llm`.
//!
//! Two forms, one per meaning. `text` and `texts` return the completion text:
//! they are plain functions, and a `schema` they take is the program's own,
//! sent to the provider as written and never used to parse or check the
//! answer. `call<T>` and `batch<T>` return a checked `T`: they are declared
//! **generic** and **runtime-generic**, so like every generic function a call
//! passes one `$TypeDescriptor` per type parameter in a trailing `$types`
//! environment — the type's structural predicate, its name, its JSON Schema,
//! and its TypeInfo reference (`codegen::runtime_descriptors`). The host sends
//! the schema to the provider, parses each response as JSON and runs the
//! predicate on it; a rejected value becomes an `ok: false` entry with
//! `reason: "invalid-output"`. So the `value` of a `Success<T>` holds only
//! what the predicate accepted, which is what lets the declared return type
//! `Completion<T>` be trusted without a cast around the call. `T` has no
//! default: it is written, or inferred from the expected type, and the call is
//! typed either way. The typechecker gates it through its rows in the
//! checked-call table (`stdlib::checked_call`; [`is_checked_call`] and
//! [`is_checked_batch`] are its hooks), and codegen emits the schema per type;
//! nothing about the descriptor is a parameter, so the written declaration is
//! the whole public API.

use std::collections::BTreeMap;

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

use super::MODULE_NAME;

/// The export names of the checked reads. Shared with the typechecker's gate
/// so the declaration and the check that keeps it sound cannot drift apart.
const LLM_CALL: &str = "call";
const LLM_BATCH: &str = "batch";
/// The text forms: plain functions, no type argument, no check.
const LLM_TEXT: &str = "text";
const LLM_TEXTS: &str = "texts";

/// The type parameter of [`LLM_CALL`] and [`LLM_BATCH`].
const CALL_TYPE_PARAM: &str = "T";

/// The type parameter as a type, for the generic return positions.
fn call_type_var() -> Type {
    Type::TypeVar(CALL_TYPE_PARAM.to_string())
}

// The checked reads, `call<T>` and `batch<T>`, are recognised by their
// package-export mangled name rather than a receiver name, which is what makes
// the typechecker's gate independent of how the symbol was imported —
// `llm.call`, `models.call` under an aliased namespace import, and a
// named-import `call` all carry this name, while a user's own `llm` binding
// never can.

/// Whether `mangled` is `call<T>`.
pub fn is_checked_call(mangled: &MangledName) -> bool {
    *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_CALL)
}

/// Whether `mangled` is `batch<T>`.
pub fn is_checked_batch(mangled: &MangledName) -> bool {
    *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_BATCH)
}

pub fn package_declaration() -> PackageDeclaration {
    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
    insert_success_interface(&mut defs);
    insert_failure_interface(&mut defs);
    insert_completion_alias(&mut defs);
    insert_model_interface(&mut defs);
    insert_fn(
        &mut defs,
        LLM_TEXT,
        vec![
            Param::new("model", Type::String),
            Param::new("prompt", Type::String),
            Param::with_default(
                "schema",
                Type::Union(vec![Type::String, Type::Undefined]),
                crate::DefaultValue::Undefined,
            ),
        ],
        completion_type(Type::String),
        "/**\n * Send one prompt to `model` and return its completion text as a \
         `Completion<string>`: `{ ok: true, value }` or `{ ok: false, reason, message }`. \
         Check `ok` before reading `value`; the compiler will not let you read it \
         otherwise.\n *\n * `schema`, when given, is a JSON Schema sent to the provider \
         as written, to constrain the output. It must be a JSON object, else a catchable `TypeError`, and it is held to the same \
         byte bound as a prompt. The answer is not parsed or checked against it: \
         `value` is the text, and a program that wants a checked value calls \
         `call<T>` instead, whose schema the compiler emits from `T`.\n *\n * \
         `ok: false` means the model did not produce a clean completion; branch on \
         `reason`, which is a closed set, not on `message`. A completion cut off at \
         the output cap is `reason: \"truncated\"` and still carries the partial \
         `text`, so a loop that only reads `ok` elements discards usable output.\n *\n \
         * Throws a `QuotaExceededError` when the execution or server token budget \
         cannot cover the call, a `RangeError` for prompt bounds, and a catchable \
         error naming the model when no provider is configured.\n * @param model \
         Model name. Call `models()` for the ones this runtime serves; an undeclared \
         name throws.\n * @param prompt The prompt text. Bounded in bytes \
         independently of the token ceiling.\n * @param schema A JSON Schema for the \
         provider, sent unchanged; the result stays text.\n * @capability llm.call \
         { model: $model, prompt_count: 1 }\n */",
    );
    insert_fn(
        &mut defs,
        LLM_TEXTS,
        vec![
            Param::new("model", Type::String),
            Param::new("prompts", Type::Array(Box::new(Type::String))),
            Param::with_default(
                "schema",
                Type::Union(vec![Type::String, Type::Undefined]),
                crate::DefaultValue::Undefined,
            ),
        ],
        Type::Array(Box::new(completion_type(Type::String))),
        "/**\n * Send every prompt to `model` and return one `Completion<string>` each, \
         positionally: `result[i]` is the text outcome of `prompts[i]`, including when \
         that element failed. An empty `prompts` returns `[]`.\n *\n * Fan-out \
         happens host-side at a bounded concurrency, so this is not sugar for a loop. \
         One failing element never discards the ones that succeeded: each element \
         carries its own `ok`, `reason` and token counts. `schema`, when given, goes \
         to the provider with every prompt and never checks an answer; it must be a \
         JSON object, and it is held to the per-prompt byte \
         bound. For checked values call `batch<T>`.\n *\n * The whole slice is \
         checked against the \
         prompt-count and prompt-size bounds, and its tokens reserved, before any \
         bytes leave the process.\n * @param model Model name. Call `models()` for \
         the ones this runtime serves.\n * @param prompts The prompts, in the order \
         the results come back. Bounded in count, and each in bytes.\n * @param \
         schema A JSON Schema for the provider, sent unchanged; the results stay \
         text.\n * @capability llm.call { model: $model, prompt_count: \
         $prompts.length }\n */",
    );
    insert_generic_fn(
        &mut defs,
        LLM_CALL,
        vec![CALL_TYPE_PARAM.to_string()],
        vec![
            Param::new("model", Type::String),
            Param::new("prompt", Type::String),
        ],
        completion_type(call_type_var()),
        "/**\n * Send one prompt to `model` and return its answer checked as a `T`: \
         `{ ok: true, value }` or `{ ok: false, reason, message }`. Check `ok` before \
         reading `value`; the compiler will not let you read it otherwise.\n *\n * \
         `call<Verdict>(model, prompt)` emits a JSON Schema for `Verdict` at compile \
         time and sends it to the provider; the response is parsed as JSON and \
         checked field by field, and `value` is the checked `Verdict`. `T` is \
         required: write it, or let the expected type supply it. For the completion \
         text call `text(model, prompt)` instead. A response that \
         is not JSON, or that does not match, is `ok: false` with `reason: \
         \"invalid-output\"` and a `message` naming the expected type; nothing is \
         coerced, and no error ever quotes the prompt or the completion.\n *\n * `T` \
         must be a type the schema can carry: no functions, no \
         `bigint`, no `Uint8Array`, no `unknown`, and no recursive type — each is \
         rejected at compile time, naming the offending field. A generic may pass \
         its own type parameter (`call<T>` inside `function f<T>`): the caller's \
         type is asked for and checked, and one with no JSON Schema throws a \
         catchable `TypeError` when the call runs.\n *\n * `ok: false` \
         means the model did not produce a clean completion; branch on `reason`, \
         which is a closed set, not on `message`. A completion cut off at the output \
         cap is `reason: \"truncated\"` and carries no text: a typed `value` could \
         never hold it.\n *\n * Throws a \
         `QuotaExceededError` when the execution or server token budget cannot cover \
         the call, a `RangeError` for prompt bounds, and a catchable error naming the \
         model when no provider is configured.\n * @param model Model name. Call \
         `models()` for the ones this runtime serves; an undeclared name throws.\n * \
         @param prompt The prompt text. Bounded in bytes independently of the token \
         ceiling.\n * @capability llm.call { model: $model, prompt_count: 1 }\n */",
    );
    insert_generic_fn(
        &mut defs,
        LLM_BATCH,
        vec![CALL_TYPE_PARAM.to_string()],
        vec![
            Param::new("model", Type::String),
            Param::new("prompts", Type::Array(Box::new(Type::String))),
        ],
        Type::Array(Box::new(completion_type(call_type_var()))),
        "/**\n * Send every prompt to `model` and return one `Completion` each, \
         positionally: `result[i]` is the outcome of `prompts[i]`, including when that \
         element failed. An empty `prompts` returns `[]`.\n *\n * Fan-out happens \
         host-side at a bounded concurrency, so this is not sugar for a loop — a \
         sequential version would misrepresent its own cost. One failing element never \
         discards the ones that succeeded: each element carries its own `ok`, `reason` \
         and token counts.\n *\n * A type argument names the shape of *one* response: \
         `batch<Verdict>(model, prompts)` returns `Completion<Verdict>[]`, each element \
         parsed and checked on its own, so a response that is not JSON or does not \
         match `Verdict` is `ok: false` with `reason: \"invalid-output\"` for that \
         element only. `T` is required; for the completion text of each prompt call \
         `texts(model, prompts)` instead.\n *\n * The whole slice is checked against the prompt-count and \
         prompt-size bounds, and its tokens reserved, before any bytes leave the \
         process — so an oversized batch is refused without partially dispatching.\n \
         * @param model Model name. Call `models()` for the ones this runtime \
         serves.\n * @param prompts The prompts, in the order the results come back. \
         Bounded in count, and each in bytes.\n * @capability llm.call { model: \
         $model, prompt_count: $prompts.length }\n */",
    );
    for name in [LLM_CALL, LLM_BATCH] {
        defs.runtime_generics
            .insert(crate::mangle::package_symbol(MODULE_NAME, name));
    }
    insert_fn(
        &mut defs,
        "models",
        Vec::new(),
        Type::Array(Box::new(model_type())),
        "/**\n * The models this runtime serves and this caller may call.\n *\n * \
         Each candidate is filtered under `llm.call` by the same `model` \
         filter that gates calling — so a listing never offers a model the caller \
         would be denied at `call` time. The list can therefore come back short, or \
         empty, and nothing in it reveals how many candidates were filtered out.\n \
         *\n * Choosing a model is the delegation decision — a cheap model for a \
         thousand classifications, a strong one for the synthesis — so branch on \
         `contextWindow`, which is a fact you can compute against. Both \
         `contextWindow` and `description` are absent when the operator declared \
         none; a program filtering on `contextWindow` drops those, which is correct \
         for a chunk-size decision, because you should not size against a number \
         nobody asserted.\n *\n * Treat `description` as advice, not fact: it is \
         operator-authored free text that steers which model your program calls.\n * \
         @capability llm.call { prompt_count: 0 }\n */",
    );
    defs
}

fn package() -> crate::Package {
    crate::Package(MODULE_NAME.to_string())
}

/// `Completion<value>`: the declared alias, so diagnostics and hovers show the
/// name a program wrote rather than the expanded union.
fn completion_type(value: Type) -> Type {
    Type::AliasRef {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "Completion"),
        package: package(),
        name: "Completion".to_string(),
        args: vec![value],
    }
}

fn success_type(value: Type) -> Type {
    Type::InterfaceRef {
        mangled: crate::mangle::package_symbol(MODULE_NAME, "Success"),
        package: package(),
        name: "Success".to_string(),
        args: vec![value],
    }
}

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

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

/// The closed set of failure reasons, as the union of their spellings, taken
/// from the runtime's own list so the two cannot drift.
fn reason_type() -> Type {
    Type::union(
        crate::runtime::llm::FailureReason::ALL
            .iter()
            .map(|reason| Type::StringLiteral(reason.as_str().to_string()))
            .collect(),
    )
}

fn insert_success_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_property(
        &mut properties,
        "ok",
        Type::BooleanLiteral(true),
        "/** `true`: the model stopped naturally and `value` holds its answer. */",
    );
    insert_property(
        &mut properties,
        "value",
        call_type_var(),
        "/** The completion text from `text`/`texts`, or from `call<T>`/`batch<T>` the parsed response, checked against `T` field by field. Only what passed the check is ever stored here. */",
    );
    insert_optional_property(
        &mut properties,
        "inputTokens",
        Type::Number,
        "/** Prompt tokens the provider reported. Absent means indeterminate, not free: a throttled call may still have been billed, so it is never `0` as a stand-in for unknown. */",
    );
    insert_optional_property(
        &mut properties,
        "outputTokens",
        Type::Number,
        "/** Completion tokens the provider reported, with the same absent-means-indeterminate rule as `inputTokens`. */",
    );
    insert_interface(
        defs,
        "Success",
        vec![CALL_TYPE_PARAM.to_string()],
        properties,
        "/** The `ok: true` arm of a `Completion<T>`: the model stopped naturally and `value` is its answer — the text, or the checked `T`. */",
    );
}

fn insert_failure_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_property(
        &mut properties,
        "ok",
        Type::BooleanLiteral(false),
        "/** `false`: this element is not a clean completion. Not \"something threw\": a truncated or content-filtered completion is `ok: false` and may still carry `text`. */",
    );
    insert_property(
        &mut properties,
        "reason",
        reason_type(),
        "/** Why this element is not a clean completion. One of `\"truncated\"`, `\"content-filtered\"`, `\"invalid-output\"`, `\"rate-limited\"`, `\"request-rejected\"`, `\"provider-unavailable\"`, `\"transport\"`, `\"cancelled\"`, `\"incomplete\"` — branch on this, not on `message`. A typed call's response that is not JSON or does not match `T` is `\"invalid-output\"`. */",
    );
    insert_property(
        &mut properties,
        "message",
        Type::String,
        "/** A fixed classification of the failure, or for `\"invalid-output\"` the type the response did not match. Never the prompt, the completion, or a provider response body. */",
    );
    insert_property(
        &mut properties,
        "retryable",
        Type::Boolean,
        "/** Whether re-sending this identical prompt could plausibly succeed. */",
    );
    insert_optional_property(
        &mut properties,
        "status",
        Type::Number,
        "/** The HTTP status the provider answered with, absent when none was observed — which is what distinguishes a dead connection from a provider that answered with an error. */",
    );
    insert_optional_property(
        &mut properties,
        "finishReason",
        Type::String,
        "/** The provider's own raw stop reason, for diagnosis only. Varies per provider and per SDK version — branch on `reason` instead. */",
    );
    insert_optional_property(
        &mut properties,
        "text",
        Type::String,
        "/** The partial text of a truncated or filtered completion, absent when the model produced none at all. Usable output: a loop that skips every `!ok` element discards it. Only `text` and `texts` carry it: a checked `call<T>` never does. */",
    );
    insert_optional_property(
        &mut properties,
        "inputTokens",
        Type::Number,
        "/** Prompt tokens the provider reported for this element, absent when indeterminate. A failed element may still have been billed. */",
    );
    insert_optional_property(
        &mut properties,
        "outputTokens",
        Type::Number,
        "/** Completion tokens the provider reported for this element, absent when indeterminate. */",
    );
    insert_interface(
        defs,
        "Failure",
        Vec::new(),
        properties,
        "/** The `ok: false` arm of a `Completion<T>`: why this element is not a clean completion, and the partial `text` when the model produced some. */",
    );
}

fn insert_completion_alias(defs: &mut PackageDeclaration) {
    defs.types.insert(
        "Completion".to_string(),
        TypeSymbol {
            name: "Completion".to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Completion"),
            declaration_span: Span::at(crate::FileId::LLM),
            kind: TypeKind::Alias {
                generics: vec![CALL_TYPE_PARAM.to_string()],
                ty: Type::union(vec![success_type(call_type_var()), failure_type()]),
                doc: crate::doc(
                    crate::FileId::LLM,
                    "/** One prompt's outcome: `Success<T>` when the model stopped naturally, `Failure` otherwise. Narrow on `ok` before reading `value`. `T` is `string` for `text` and `texts`. Constructed only by this package's calls, but a plain object of the same shape is one too. */",
                ),
            },
        },
    );
}

fn insert_model_interface(defs: &mut PackageDeclaration) {
    let mut properties = BTreeMap::new();
    insert_property(
        &mut properties,
        "name",
        Type::String,
        "/** The model name, exactly as `call` and `batch` expect it. */",
    );
    insert_optional_property(
        &mut properties,
        "description",
        Type::String,
        "/** Operator-authored deployment intent, absent when none was declared. Advice, not fact — it is free text that steers model selection, so weigh it after `contextWindow`. Always a single line, within a fixed length bound. */",
    );
    insert_optional_property(
        &mut properties,
        "contextWindow",
        Type::Number,
        "/** The model's context window in tokens, absent when the operator declared none. Absent means unknown, not zero, and there is no fallback table — a program sizing chunks should drop a model rather than guess. */",
    );
    insert_interface(
        defs,
        "Model",
        Vec::new(),
        properties,
        "/** One model this caller may call. Constructed only by `models()`, and the list carries nothing — no index, no position, no count — derived from the candidates the policy filtered out. */",
    );
}

/// A data-only interface whose values are ordinary objects: no methods, no
/// host getters, so property reads, `Object.keys`, `as Record<string, unknown>`
/// and structural casts all go through the shared object shape.
fn insert_interface(
    defs: &mut PackageDeclaration,
    name: &str,
    generics: Vec<String>,
    properties: BTreeMap<String, PropertySig>,
    doc: &str,
) {
    defs.types.insert(
        name.to_string(),
        TypeSymbol {
            name: name.to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
            declaration_span: Span::at(crate::FileId::LLM),
            kind: TypeKind::Interface {
                index: None,
                generics,
                methods: BTreeMap::new(),
                properties,
                dispatch: Dispatch::VTable,
                doc: crate::doc(crate::FileId::LLM, doc),
            },
        },
    );
}

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

/// An optional property is an absent key when there is no value, so
/// `Object.keys` lists exactly what `JSON.stringify` writes.
fn insert_optional_property(
    properties: &mut BTreeMap<String, PropertySig>,
    name: &str,
    ty: Type,
    doc: &str,
) {
    properties.insert(
        name.to_string(),
        PropertySig {
            ty,
            readonly: true,
            optional: true,
            intrinsic: false,
            doc: crate::doc(crate::FileId::LLM, doc),
        },
    );
}

fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
    insert_generic_fn(defs, name, Vec::new(), params, ret, doc);
}

fn insert_generic_fn(
    defs: &mut PackageDeclaration,
    name: &str,
    generics: Vec<String>,
    params: Vec<Param>,
    ret: Type,
    doc: &str,
) {
    defs.values.insert(
        name.to_string(),
        ValueSymbol {
            name: name.to_string(),
            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
            declaration_span: Span::at(crate::FileId::LLM),
            kind: ValueKind::Function {
                generics,
                params,
                ret,
                type_predicate: None,
                doc: crate::doc(crate::FileId::LLM, doc),
            },
        },
    );
}