Skip to content

Commit dbc0564

Browse files
committed
fix(host): finish typed catalog integration address
1 parent c023ca5 commit dbc0564

3 files changed

Lines changed: 163 additions & 14 deletions

File tree

crates/rustscript/tests/lsp_resource_types.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,18 @@ fn signature_help_shows_borrow_resource_and_value_params() {
538538
label.contains("sql: string"),
539539
"signature must show the value parameter: {label}"
540540
);
541+
assert!(
542+
label.contains("params: array<SqliteValue>"),
543+
"signature must show typed SQLite parameters: {label}"
544+
);
545+
assert!(
546+
label.contains("limits: SqliteLimits"),
547+
"signature must show typed SQLite limits: {label}"
548+
);
549+
assert!(
550+
label.contains("-> SqliteQueryResult"),
551+
"signature must show the typed SQLite query result: {label}"
552+
);
541553
}
542554

543555
// ---------------------------------------------------------------------------

docs/sqlite.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ The embedding policy controls the allowed database root, unsafe-SQL capability,
1616
ceilings. Configure that policy before opening a connection. Each operation is asynchronous;
1717
the VM resumes after the host operation completes.
1818

19+
## Compiler and editor catalog boundary
20+
21+
The typed SQLite catalog is a schema-only surface and does not construct a VM or link
22+
`rusqlite`. `sqlite_host_catalog` and the SQLite entries in `standard_host_catalog` remain
23+
available whenever the `runtime` feature is compiled, including builds without the `sqlite`
24+
feature. Catalog-aware compiler callers and the LSP use these declarations for named-struct
25+
field access and exact host signatures.
26+
27+
The `sqlite` feature controls the executable SQLite module, generated SQLite namespace and
28+
callables, the `rusqlite` dependency, and SQLite registration exports. A runtime build without
29+
that feature can inspect the editor/compiler contract but has no SQLite implementation to bind;
30+
execution requires a build with `sqlite` enabled and the SQLite module registered.
31+
1932
## Open options (`SqliteOpenOptions`)
2033

2134
```rust

tests/typed_host_no_dynamic_contract_tests.rs

Lines changed: 138 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
#![cfg(all(
1+
use std::any::Any;
2+
use std::collections::BTreeSet;
3+
4+
#[cfg(all(
5+
feature = "runtime",
26
feature = "http-client",
3-
feature = "sqlite",
47
not(target_family = "wasm")
58
))]
6-
7-
use std::collections::BTreeSet;
8-
9-
use vm::{
10-
HostApiCatalog, HostStructField, HostTypeSchema, http_host_catalog, jit_host_catalog,
11-
sqlite_host_catalog, standard_host_catalog,
12-
};
9+
use vm::http_host_catalog;
10+
#[cfg(all(feature = "runtime", not(target_arch = "wasm32")))]
11+
use vm::sqlite_host_catalog;
12+
#[cfg(feature = "runtime")]
13+
use vm::{HostApiCatalog, jit_host_catalog, standard_host_catalog};
14+
use vm::{HostStructField, HostTypeSchema};
1315

1416
fn assert_no_public_dynamic_root(path: &str, schema: &HostTypeSchema) {
1517
fn visit(path: &str, schema: &HostTypeSchema, seen: &mut BTreeSet<String>) {
1618
match schema {
1719
HostTypeSchema::Map(_) | HostTypeSchema::Unknown => {
1820
panic!("public host schema {path} exposes {schema:?}")
1921
}
20-
HostTypeSchema::Array(inner) | HostTypeSchema::Optional(inner) => {
21-
visit(path, inner, seen)
22-
}
22+
HostTypeSchema::Array(inner) => visit(&format!("{path}[]"), inner, seen),
23+
HostTypeSchema::Optional(inner) => visit(&format!("{path}?"), inner, seen),
2324
HostTypeSchema::Named { name, fields } => {
2425
if !seen.insert(name.clone()) {
2526
return;
@@ -48,6 +49,88 @@ fn assert_no_public_dynamic_root(path: &str, schema: &HostTypeSchema) {
4849
visit(path, schema, &mut BTreeSet::new());
4950
}
5051

52+
fn panic_text(payload: Box<dyn Any + Send>) -> String {
53+
if let Some(message) = payload.downcast_ref::<String>() {
54+
return message.clone();
55+
}
56+
if let Some(message) = payload.downcast_ref::<&str>() {
57+
return (*message).to_string();
58+
}
59+
"non-string panic payload".to_string()
60+
}
61+
62+
fn assert_rejects_dynamic_schema(
63+
path: &str,
64+
schema: HostTypeSchema,
65+
expected_path: &str,
66+
expected_kind: &str,
67+
) {
68+
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
69+
assert_no_public_dynamic_root(path, &schema);
70+
}))
71+
.expect_err("dynamic schema must be rejected");
72+
let message = panic_text(payload);
73+
assert!(
74+
message.contains(expected_path),
75+
"diagnostic should identify {expected_path}, got {message}"
76+
);
77+
assert!(
78+
message.contains(expected_kind),
79+
"diagnostic should identify {expected_kind}, got {message}"
80+
);
81+
}
82+
83+
type SchemaWrapper = fn(HostTypeSchema) -> HostTypeSchema;
84+
85+
fn array_of(inner: HostTypeSchema) -> HostTypeSchema {
86+
HostTypeSchema::Array(Box::new(inner))
87+
}
88+
89+
fn optional_of(inner: HostTypeSchema) -> HostTypeSchema {
90+
HostTypeSchema::Optional(Box::new(inner))
91+
}
92+
93+
fn named_field_of(inner: HostTypeSchema) -> HostTypeSchema {
94+
HostTypeSchema::named_struct("Envelope", vec![HostStructField::new("payload", inner)])
95+
}
96+
97+
fn callable_param_of(inner: HostTypeSchema) -> HostTypeSchema {
98+
HostTypeSchema::Callable {
99+
params: vec![inner],
100+
result: Box::new(HostTypeSchema::Int),
101+
}
102+
}
103+
104+
fn callable_result_of(inner: HostTypeSchema) -> HostTypeSchema {
105+
HostTypeSchema::Callable {
106+
params: vec![HostTypeSchema::Int],
107+
result: Box::new(inner),
108+
}
109+
}
110+
111+
const NESTED_DYNAMIC_CASES: &[(&str, SchemaWrapper, &str)] = &[
112+
("array", array_of, "[]"),
113+
("optional", optional_of, "?"),
114+
("named field", named_field_of, ".Envelope.payload"),
115+
("callable param", callable_param_of, ".callback_param[0]"),
116+
("callable result", callable_result_of, ".callback_result"),
117+
];
118+
119+
#[test]
120+
fn recursive_walker_rejects_nested_dynamic_schemas_with_paths() {
121+
for (kind, dynamic) in [
122+
("Map", HostTypeSchema::Map(Box::new(HostTypeSchema::Int))),
123+
("Unknown", HostTypeSchema::Unknown),
124+
] {
125+
for (case, wrap, suffix) in NESTED_DYNAMIC_CASES {
126+
let root = format!("nested::{kind}::{case}");
127+
let expected_path = format!("{root}{suffix}");
128+
assert_rejects_dynamic_schema(&root, wrap(dynamic.clone()), &expected_path, kind);
129+
}
130+
}
131+
}
132+
133+
#[cfg(feature = "runtime")]
51134
fn assert_no_public_dynamic_schema(catalog_name: &str, catalog: &HostApiCatalog) {
52135
for schema in catalog.structs() {
53136
for field in &schema.fields {
@@ -92,10 +175,51 @@ fn recursive_named_struct_walk_stops_at_repeated_named_type() {
92175
assert_no_public_dynamic_root("recursive::node", &recursive_named_schema());
93176
}
94177

178+
#[cfg(feature = "runtime")]
95179
#[test]
96180
fn affected_public_host_catalogs_have_no_reachable_map_or_unknown() {
97-
assert_no_public_dynamic_schema("http", &http_host_catalog());
98-
assert_no_public_dynamic_schema("sqlite", &sqlite_host_catalog());
99181
assert_no_public_dynamic_schema("jit", &jit_host_catalog());
100182
assert_no_public_dynamic_schema("standard", &standard_host_catalog());
183+
#[cfg(all(feature = "http-client", not(target_family = "wasm")))]
184+
assert_no_public_dynamic_schema("http", &http_host_catalog());
185+
#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
186+
assert_no_public_dynamic_schema("sqlite", &sqlite_host_catalog());
187+
}
188+
189+
#[cfg(all(feature = "runtime", not(feature = "sqlite")))]
190+
#[test]
191+
fn standard_catalog_keeps_sqlite_editor_schema_without_sqlite_runtime() {
192+
let sqlite = sqlite_host_catalog();
193+
assert!(
194+
sqlite.function("sqlite::query").is_some(),
195+
"the standalone editor/compiler catalog must retain SQLite declarations"
196+
);
197+
let query = sqlite
198+
.function("sqlite::query")
199+
.expect("SQLite query declaration");
200+
let value = sqlite
201+
.struct_named("SqliteValue")
202+
.expect("SQLite value named struct");
203+
assert_eq!(
204+
query.params[2].ty,
205+
HostTypeSchema::Array(Box::new(value.as_type())),
206+
"SQLite query params must stay typed without the runtime feature"
207+
);
208+
assert_no_public_dynamic_schema("sqlite", &sqlite);
209+
210+
let catalog = standard_host_catalog();
211+
assert!(
212+
catalog.function("sqlite::open").is_some(),
213+
"the editor/compiler catalog must retain SQLite schema declarations"
214+
);
215+
assert!(
216+
catalog.struct_named("SqliteOpenOptions").is_some(),
217+
"the editor/compiler catalog must retain SQLite named structs"
218+
);
219+
assert!(
220+
vm::default_host_callables()
221+
.iter()
222+
.all(|callable| !callable.name.starts_with("sqlite::")),
223+
"the executable default host surface must remain feature-gated"
224+
);
101225
}

0 commit comments

Comments
 (0)