Skip to main content

submilli_engine/stdlib/skills/
declaration.rs

1//! Type surface of `submilli:skills`.
2
3use std::collections::BTreeMap;
4
5use crate::{
6    Dispatch, MethodSig, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind, TypeSymbol,
7    ValueKind, ValueSymbol,
8};
9
10use super::MODULE_NAME;
11
12pub fn package_declaration() -> PackageDeclaration {
13    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
14    insert_interface(
15        &mut defs,
16        "SkillInfo",
17        skill_properties(false),
18        BTreeMap::new(),
19        "/** One skill this caller may load. Constructed only by `list()`. */",
20    );
21    insert_interface(
22        &mut defs,
23        "Skill",
24        skill_properties(true),
25        skill_methods(),
26        "/** A loaded skill. Read bundled files with `skill.readFile(path)`. Skills are never mounted or accessible through `submilli:fs`. Constructed only by `load()`. */",
27    );
28    insert_fn(
29        &mut defs,
30        "list",
31        Vec::new(),
32        Type::Array(Box::new(interface_type("SkillInfo"))),
33        "/**\n * The skills this caller may load.\n *\n * Each skill the harness offers is \
34         filtered under `skill.load` by the same `name` filter that gates `load`, so \
35         the list never offers a skill the caller would be denied. Nothing in it \
36         reveals how many were filtered out.\n * @capability skill.load\n */",
37    );
38    insert_fn(
39        &mut defs,
40        "load",
41        vec![Param::new("name", Type::String)],
42        interface_type("Skill"),
43        "/**\n * Load the skill `name`: its description, instructions and bundled file names.\n *\n * \
44         A name that is not one skill name — empty, `.` or `..`, containing `/`, \
45         `\\` or NUL, or over 4096 bytes — throws a `RangeError` without asking the \
46         harness. Throws a catchable error when the harness has no skill of that name, \
47         or when this runtime has no skill provider.\n * @param name Skill name, as \
48         `list()` returns it.\n * @capability skill.load { name: $name }\n */",
49    );
50    defs
51}
52
53fn skill_methods() -> BTreeMap<String, MethodSig> {
54    BTreeMap::from([(
55        "readFile".to_string(),
56        MethodSig {
57            optional: false,
58            generics: Vec::new(),
59            params: vec![Param::new("path", Type::String)],
60            ret: Type::String,
61            predicate: None,
62            doc: crate::doc(
63                crate::FileId::SKILLS,
64                "/** Read a bundled file as text. Pass a path from `files`, relative to this skill with `/` separators. Absolute paths, empty, `.` or `..` segments, `\\`, `:` or NUL, and paths over 4096 bytes throw a `RangeError` before asking the provider. A missing file throws a catchable error. Files are never accessible through `submilli:fs`. The `skill.load` policy is checked again using this skill's name.\n * @param path The file's path inside this skill.\n * @capability skill.load\n */",
65            ),
66        },
67    )])
68}
69
70fn interface_type(name: &str) -> Type {
71    Type::InterfaceRef {
72        mangled: crate::mangle::package_symbol(MODULE_NAME, name),
73        package: crate::Package(MODULE_NAME.to_string()),
74        name: name.to_string(),
75        args: Vec::new(),
76    }
77}
78
79/// `name` and `description`, plus instructions and file names for a loaded skill.
80fn skill_properties(loaded: bool) -> BTreeMap<String, PropertySig> {
81    let mut properties = BTreeMap::new();
82    insert_property(
83        &mut properties,
84        "name",
85        Type::String,
86        "/** The skill name, exactly as `load` and `readFile` expect it. */",
87    );
88    insert_property(
89        &mut properties,
90        "description",
91        Type::union(vec![Type::String, Type::Undefined]),
92        "/** What the skill is for, or `undefined` when the harness gives none. */",
93    );
94    if loaded {
95        insert_property(
96            &mut properties,
97            "files",
98            Type::Array(Box::new(Type::String)),
99            "/** Bundled file names relative to this skill, without reading their contents. Pass each to `skill.readFile(path)`. These are not filesystem paths and cannot be read through `submilli:fs`. Invalid paths are omitted. */",
100        );
101        insert_property(
102            &mut properties,
103            "content",
104            Type::String,
105            "/** The skill's instructions. */",
106        );
107    }
108    properties
109}
110
111fn insert_interface(
112    defs: &mut PackageDeclaration,
113    name: &str,
114    properties: BTreeMap<String, PropertySig>,
115    methods: BTreeMap<String, MethodSig>,
116    doc: &str,
117) {
118    defs.types.insert(
119        name.to_string(),
120        TypeSymbol {
121            name: name.to_string(),
122            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
123            declaration_span: Span::at(crate::FileId::SKILLS),
124            kind: TypeKind::Interface {
125                index: None,
126                generics: Vec::new(),
127                methods,
128                properties,
129                dispatch: Dispatch::Direct,
130                doc: crate::doc(crate::FileId::SKILLS, doc),
131            },
132        },
133    );
134}
135
136fn insert_property(
137    properties: &mut BTreeMap<String, PropertySig>,
138    name: &str,
139    ty: Type,
140    doc: &str,
141) {
142    properties.insert(
143        name.to_string(),
144        PropertySig {
145            ty,
146            readonly: true,
147            optional: false,
148            intrinsic: false,
149            doc: crate::doc(crate::FileId::SKILLS, doc),
150        },
151    );
152}
153
154fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
155    defs.values.insert(
156        name.to_string(),
157        ValueSymbol {
158            name: name.to_string(),
159            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
160            declaration_span: Span::at(crate::FileId::SKILLS),
161            kind: ValueKind::Function {
162                generics: Vec::new(),
163                params,
164                ret,
165                type_predicate: None,
166                doc: crate::doc(crate::FileId::SKILLS, doc),
167            },
168        },
169    );
170}