1use std::collections::{BTreeMap, BTreeSet};
8use std::fmt;
9
10use sha2::{Digest, Sha256};
11
12use crate::kernel::Form;
13
14use super::wit_format;
15use super::wit_parser::{self, Document, Interface, Type, TypeDecl};
16use super::{HaraValueType, Lifting, Lowering, WasmInterface};
17
18pub const WIT_IR_SCHEMA: &str = "hara.wasm-wit/0-alpha";
19pub const WIT_MANIFEST_SCHEMA: &str = "hara.wasm-wit-manifest/0-alpha";
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum WitDiagnosticSeverity {
23 Lossy,
24 Unsupported,
25}
26
27impl WitDiagnosticSeverity {
28 pub(super) fn as_keyword(self) -> &'static str {
29 match self {
30 Self::Lossy => "lossy",
31 Self::Unsupported => "unsupported",
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
37pub struct WitDiagnostic {
38 pub severity: WitDiagnosticSeverity,
39 pub code: String,
40 pub path: String,
41 pub message: String,
42}
43
44impl fmt::Display for WitDiagnostic {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(
47 formatter,
48 "{} {} at {}: {}",
49 self.severity.as_keyword(),
50 self.code,
51 self.path,
52 self.message
53 )
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
58pub enum WitRoute {
59 DirectImport,
60 HtaRequire,
61}
62
63impl WitRoute {
64 pub fn as_keyword(self) -> &'static str {
65 match self {
66 Self::DirectImport => "import",
67 Self::HtaRequire => "require",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct WitImportOptions {
74 pub namespace: Option<String>,
75 pub module: Option<String>,
76 pub world: Option<String>,
77 pub interface: Option<String>,
78 pub strict: bool,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct WitImportArtifact {
83 pub namespace: String,
84 pub world: Option<String>,
85 pub interface: String,
86 pub route: WitRoute,
87 pub interface_source: String,
88 pub normalized_ir: String,
89 pub diagnostics: Vec<WitDiagnostic>,
90 pub source_digest: String,
91 pub interface_digest: String,
92}
93
94impl WitImportArtifact {
95 pub fn manifest_source(&self, origin: &str) -> String {
96 Form::Map(vec![
97 (keyword("schema"), string(WIT_MANIFEST_SCHEMA)),
98 (keyword("origin"), string(origin)),
99 (keyword("source-digest"), string(&self.source_digest)),
100 (keyword("interface-digest"), string(&self.interface_digest)),
101 (keyword("route"), keyword(self.route.as_keyword())),
102 (
103 keyword("diagnostics"),
104 Form::Vector(
105 self.diagnostics
106 .iter()
107 .map(wit_format::diagnostic_form)
108 .collect(),
109 ),
110 ),
111 ])
112 .to_string()
113 }
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct WitProjectionOptions {
118 pub package: Option<String>,
119 pub interface: Option<String>,
120 pub world: Option<String>,
121 pub strict: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct WitProjectionArtifact {
126 pub source: String,
127 pub diagnostics: Vec<WitDiagnostic>,
128}
129
130pub fn import_wit(
131 source: &str,
132 origin: &str,
133 options: &WitImportOptions,
134) -> Result<WitImportArtifact, String> {
135 let document = wit_parser::parse(source)
136 .map_err(|error| format!("wasm-wit/malformed {origin}: {error}"))?;
137 let (interface_name, world_name, interface, mut diagnostics) =
138 select_interface(&document, options, origin)?;
139 let namespace = options
140 .namespace
141 .clone()
142 .unwrap_or_else(|| default_namespace(document.package.as_deref(), &interface_name));
143 if !valid_namespace(&namespace) {
144 return Err(format!(
145 "wasm-wit/malformed {origin}: namespace must be a qualified lower-case name"
146 ));
147 }
148 let module = options
149 .module
150 .clone()
151 .unwrap_or_else(|| "module.wasm".to_owned());
152 if !valid_module(&module) {
153 return Err(format!(
154 "wasm-wit/malformed {origin}: module must be a safe relative .wasm path"
155 ));
156 }
157
158 let mut context = MappingContext {
159 types: &interface.types,
160 diagnostics: &mut diagnostics,
161 memory: false,
162 hta: !interface.resources.is_empty(),
163 reported: BTreeSet::new(),
164 };
165 for resource in &interface.resources {
166 context.diagnostic(
167 WitDiagnosticSeverity::Lossy,
168 "resource",
169 resource,
170 "resources require explicit HTA handle ownership and release semantics",
171 );
172 }
173 let mut exports = Vec::new();
174 for function in &interface.functions {
175 if function.async_ {
176 context.diagnostic(
177 WitDiagnosticSeverity::Unsupported,
178 "async",
179 &function.name,
180 "async WIT functions require an HTA provider",
181 );
182 context.hta = true;
183 }
184 let arguments = function
185 .arguments
186 .iter()
187 .enumerate()
188 .map(|(index, (name, ty))| {
189 let path = format!("{}.argument.{index}", function.name);
190 let mapped = context.map_type(ty, &path, &function.name);
191 (name.clone(), mapped)
192 })
193 .collect::<Vec<_>>();
194 let result = function
195 .result
196 .as_ref()
197 .map(|ty| context.map_type(ty, &format!("{}.result", function.name), &function.name));
198 exports.push((function, arguments, result));
199 }
200 for (name, declaration) in &interface.types {
201 if let TypeDecl::Unsupported(feature) = declaration {
202 context.diagnostic(
203 WitDiagnosticSeverity::Unsupported,
204 feature,
205 &format!("type.{name}"),
206 "the declaration is not represented by the Hara binding IR",
207 );
208 }
209 }
210 if !document
211 .worlds
212 .get(world_name.as_deref().unwrap_or(""))
213 .map_or(true, |world| world.imports.is_empty())
214 {
215 context.diagnostic(
216 WitDiagnosticSeverity::Unsupported,
217 "world-import",
218 "world",
219 "imported host interfaces need an explicit HTA capability contract",
220 );
221 context.hta = true;
222 }
223 if exports.is_empty() {
224 context.diagnostic(
225 WitDiagnosticSeverity::Unsupported,
226 "empty-interface",
227 "interface",
228 "an empty interface cannot seed a bindable Hara export map",
229 );
230 }
231 let memory = context.memory;
232 let hta = context.hta;
233 drop(context);
234 diagnostics.sort();
235 if options.strict && !diagnostics.is_empty() {
236 return Err(strict_error(origin, &diagnostics));
237 }
238
239 let route = if hta {
240 WitRoute::HtaRequire
241 } else {
242 WitRoute::DirectImport
243 };
244 let interface_source =
245 wit_format::skeleton_source(&namespace, &module, &exports, memory, route);
246 let normalized_ir = wit_format::normalized_source(
247 &namespace,
248 document.package.as_deref(),
249 world_name.as_deref(),
250 &interface_name,
251 route,
252 &interface,
253 &exports,
254 &diagnostics,
255 source,
256 origin,
257 );
258 Ok(WitImportArtifact {
259 namespace,
260 world: world_name,
261 interface: interface_name,
262 route,
263 source_digest: digest(source.as_bytes()),
264 interface_digest: digest(interface_source.as_bytes()),
265 interface_source,
266 normalized_ir,
267 diagnostics,
268 })
269}
270
271pub fn project_wit(
272 interface: &WasmInterface,
273 options: &WitProjectionOptions,
274) -> Result<WitProjectionArtifact, String> {
275 let mut diagnostics = Vec::new();
276 let mut functions = Vec::new();
277 for export in &interface.exports {
278 let args = export
279 .arguments
280 .iter()
281 .enumerate()
282 .map(|(index, argument)| {
283 wit_format::wit_projection_type(
284 &argument.hara_type,
285 argument.wasm_type,
286 argument.lowering,
287 argument.ownership,
288 &mut diagnostics,
289 &format!("{}.argument.{index}", export.name),
290 )
291 .map(|ty| (argument.name.clone(), ty))
292 })
293 .collect::<Option<Vec<_>>>();
294 let result = wit_format::wit_projection_type(
295 &export.returns.hara_type,
296 export.returns.wasm_type,
297 export.returns.lifting.map(|value| match value {
298 Lifting::Direct => Lowering::Direct,
299 Lifting::PointerLength => Lowering::PointerLength,
300 Lifting::PackedI64 => Lowering::PointerLength,
301 }),
302 export.returns.ownership,
303 &mut diagnostics,
304 &format!("{}.result", export.name),
305 );
306 if let (Some(args), Some(result)) = (args, result) {
307 functions.push((export.name.clone(), args, result));
308 }
309 if export.asynchronous {
310 wit_format::diagnostic(
311 &mut diagnostics,
312 WitDiagnosticSeverity::Unsupported,
313 "async",
314 &export.name,
315 "HTA asynchronous exports are not part of an exact WIT projection",
316 );
317 }
318 if export.errors.is_some() {
319 wit_format::diagnostic(
320 &mut diagnostics,
321 WitDiagnosticSeverity::Unsupported,
322 "error-mapping",
323 &export.name,
324 "Hara error conventions need an explicit WIT result definition",
325 );
326 }
327 if !export.capabilities.is_empty() || !interface.capabilities.is_empty() {
328 wit_format::diagnostic(
329 &mut diagnostics,
330 WitDiagnosticSeverity::Unsupported,
331 "capability",
332 &export.name,
333 "Hara capabilities are not representable in a WIT interface",
334 );
335 }
336 }
337 diagnostics.sort();
338 if options.strict && !diagnostics.is_empty() {
339 return Err(strict_error(&interface.namespace, &diagnostics));
340 }
341 let interface_name = options.interface.clone().unwrap_or_else(|| {
342 interface
343 .namespace
344 .rsplit('.')
345 .next()
346 .unwrap_or("interface")
347 .into()
348 });
349 let package = options
350 .package
351 .clone()
352 .unwrap_or_else(|| default_package(&interface.namespace));
353 let world = options
354 .world
355 .clone()
356 .unwrap_or_else(|| format!("{interface_name}-world"));
357 if !valid_wit_name(&interface_name) || !valid_wit_name(&world) {
358 return Err("wasm-wit/malformed: projection names must be valid WIT identifiers".into());
359 }
360 let mut source = format!("package {package};\n\ninterface {interface_name} {{\n");
361 for (name, args, result) in functions {
362 source.push_str(" ");
363 source.push_str(&name);
364 source.push_str(": func(");
365 source.push_str(
366 &args
367 .into_iter()
368 .map(|(name, ty)| format!("{name}: {ty}"))
369 .collect::<Vec<_>>()
370 .join(", "),
371 );
372 source.push(')');
373 if result != "unit" {
374 source.push_str(" -> ");
375 source.push_str(&result);
376 }
377 source.push_str(";\n");
378 }
379 source.push_str("}\n\nworld ");
380 source.push_str(&world);
381 source.push_str(" {\n export ");
382 source.push_str(&interface_name);
383 source.push_str(";\n}\n");
384 Ok(WitProjectionArtifact {
385 source,
386 diagnostics,
387 })
388}
389
390fn select_interface(
391 document: &Document,
392 options: &WitImportOptions,
393 origin: &str,
394) -> Result<(String, Option<String>, Interface, Vec<WitDiagnostic>), String> {
395 let mut diagnostics = Vec::new();
396 let world_name = options.world.clone().or_else(|| {
397 (document.worlds.len() == 1)
398 .then(|| document.worlds.keys().next().cloned())
399 .flatten()
400 });
401 if options.world.is_some()
402 && !document
403 .worlds
404 .contains_key(options.world.as_ref().unwrap())
405 {
406 return Err(format!(
407 "wasm-wit/malformed {origin}: requested world is not declared"
408 ));
409 }
410 let interface_name = if let Some(name) = options.interface.clone() {
411 name
412 } else if let Some(world) = world_name
413 .as_ref()
414 .and_then(|name| document.worlds.get(name))
415 {
416 let matches = world
417 .exports
418 .iter()
419 .filter(|name| document.interfaces.contains_key(*name))
420 .cloned()
421 .collect::<Vec<_>>();
422 if matches.len() != 1 {
423 diagnostics.push(WitDiagnostic {
424 severity: WitDiagnosticSeverity::Unsupported,
425 code: "world-export-selection".into(),
426 path: "world".into(),
427 message: "a world must select exactly one declared interface".into(),
428 });
429 }
430 matches
431 .first()
432 .cloned()
433 .or_else(|| document.interfaces.keys().next().cloned())
434 .ok_or_else(|| format!("wasm-wit/malformed {origin}: world has no interface export"))?
435 } else if document.interfaces.len() == 1 {
436 document.interfaces.keys().next().cloned().unwrap()
437 } else {
438 diagnostics.push(WitDiagnostic {
439 severity: WitDiagnosticSeverity::Unsupported,
440 code: "interface-selection".into(),
441 path: "document".into(),
442 message: "multiple interfaces require --interface or a world export".into(),
443 });
444 document
445 .interfaces
446 .keys()
447 .next()
448 .cloned()
449 .ok_or_else(|| format!("wasm-wit/malformed {origin}: no interface was declared"))?
450 };
451 let interface = document
452 .interfaces
453 .get(&interface_name)
454 .cloned()
455 .ok_or_else(|| {
456 format!("wasm-wit/malformed {origin}: interface {interface_name} is not declared")
457 })?;
458 Ok((interface_name, world_name, interface, diagnostics))
459}
460
461struct MappingContext<'a> {
462 types: &'a BTreeMap<String, TypeDecl>,
463 diagnostics: &'a mut Vec<WitDiagnostic>,
464 memory: bool,
465 hta: bool,
466 reported: BTreeSet<String>,
467}
468
469impl MappingContext<'_> {
470 fn map_type(&mut self, ty: &Type, path: &str, context: &str) -> HaraValueType {
471 match ty {
472 Type::Atom(name) => match name.as_str() {
473 "bool" => HaraValueType::Boolean,
474 "u8" | "u16" | "u32" | "s8" | "s16" => {
475 self.diagnostic(
476 WitDiagnosticSeverity::Lossy,
477 "integer-width",
478 path,
479 &format!("WIT {name} is lowered to the existing Hara i32 scalar"),
480 );
481 HaraValueType::I32
482 }
483 "s32" => HaraValueType::I32,
484 "u64" => {
485 self.diagnostic(
486 WitDiagnosticSeverity::Lossy,
487 "integer-width",
488 path,
489 "WIT u64 is lowered to the existing Hara i64 scalar",
490 );
491 HaraValueType::I64
492 }
493 "s64" => HaraValueType::I64,
494 "f32" => HaraValueType::F32,
495 "f64" => HaraValueType::F64,
496 "char" => {
497 self.diagnostic(
498 WitDiagnosticSeverity::Lossy,
499 "char",
500 path,
501 "WIT char is lowered to the existing Hara i32 scalar",
502 );
503 HaraValueType::I32
504 }
505 "string" => {
506 self.memory = true;
507 HaraValueType::String
508 }
509 "unit" => HaraValueType::Void,
510 name => match self.types.get(name) {
511 Some(TypeDecl::Alias(value)) => self.map_type(value, path, context),
512 Some(TypeDecl::Record(_)) => {
513 self.lossy_named("record", name, path);
514 HaraValueType::Record(name.into())
515 }
516 Some(TypeDecl::Variant(_)) => {
517 self.lossy_named("variant", name, path);
518 HaraValueType::Variant(name.into())
519 }
520 Some(TypeDecl::Resource) => {
521 self.hta = true;
522 self.diagnostic(
523 WitDiagnosticSeverity::Lossy,
524 "resource",
525 path,
526 "resource is represented as an HTA-owned Hara handle",
527 );
528 HaraValueType::Handle(name.into())
529 }
530 Some(TypeDecl::Unsupported(_)) | None => {
531 self.diagnostic(
532 WitDiagnosticSeverity::Unsupported,
533 "type",
534 path,
535 &format!("named type {name} is not represented"),
536 );
537 HaraValueType::Variant(name.into())
538 }
539 },
540 },
541 Type::List(value) => {
542 if matches!(value.as_ref(), Type::Atom(name) if name == "u8") {
543 self.memory = true;
544 HaraValueType::Bytes
545 } else {
546 self.hta = true;
547 self.diagnostic(
548 WitDiagnosticSeverity::Lossy,
549 "list",
550 path,
551 "only list<u8> has an exact existing Hara bytes representation",
552 );
553 HaraValueType::Record(format!("{}-list", context))
554 }
555 }
556 Type::Option(value) => {
557 self.hta = true;
558 self.diagnostic(
559 WitDiagnosticSeverity::Lossy,
560 "option",
561 path,
562 &format!(
563 "option<{}> is represented as an HTA variant until a canonical Hara option owner is authored",
564 wit_parser::type_label(value)
565 ),
566 );
567 HaraValueType::Variant(format!("{}-option", context))
568 }
569 Type::Result(ok, error) => {
570 self.hta = true;
571 self.diagnostic(
572 WitDiagnosticSeverity::Lossy,
573 "result",
574 path,
575 &format!(
576 "result<{}, {}> is represented as an HTA variant until Hara error ownership is authored",
577 ok.as_deref()
578 .map(wit_parser::type_label)
579 .unwrap_or_else(|| "unit".into()),
580 error
581 .as_deref()
582 .map(wit_parser::type_label)
583 .unwrap_or_else(|| "unit".into())
584 ),
585 );
586 HaraValueType::Variant(format!("{}-result", context))
587 }
588 Type::Tuple(values) => {
589 self.hta = true;
590 self.diagnostic(
591 WitDiagnosticSeverity::Lossy,
592 "tuple",
593 path,
594 &format!(
595 "tuple of {} values needs an authored Hara record",
596 values.len()
597 ),
598 );
599 HaraValueType::Record(format!("{}-tuple", context))
600 }
601 }
602 }
603
604 fn lossy_named(&mut self, kind: &str, name: &str, path: &str) {
605 self.hta = true;
606 self.diagnostic(
607 WitDiagnosticSeverity::Lossy,
608 kind,
609 path,
610 &format!("{kind} {name} is seeded by name; fields and ownership remain canonical .hal"),
611 );
612 }
613
614 fn diagnostic(
615 &mut self,
616 severity: WitDiagnosticSeverity,
617 code: &str,
618 path: &str,
619 message: &str,
620 ) {
621 let key = format!("{}:{code}:{path}:{message}", severity.as_keyword());
622 if self.reported.insert(key) {
623 self.diagnostics.push(WitDiagnostic {
624 severity,
625 code: code.into(),
626 path: path.into(),
627 message: message.into(),
628 });
629 }
630 }
631}
632
633fn strict_error(origin: &str, diagnostics: &[WitDiagnostic]) -> String {
634 let details = diagnostics
635 .iter()
636 .map(ToString::to_string)
637 .collect::<Vec<_>>()
638 .join("\n");
639 format!("wasm-wit/strict {origin}: unsupported or lossy mappings:\n{details}")
640}
641
642fn default_namespace(package: Option<&str>, interface: &str) -> String {
643 let package = package.unwrap_or("hara:wit");
644 let package = package.split('@').next().unwrap_or(package);
645 let package = package.replace(':', ".");
646 let namespace = if package.rsplit('.').next() == Some(interface) {
647 package
648 } else {
649 format!("{package}.{interface}")
650 };
651 if valid_namespace(&namespace) {
652 namespace
653 } else {
654 format!("hara.wit.{interface}")
655 }
656}
657
658fn default_package(namespace: &str) -> String {
659 let mut parts = namespace.split('.');
660 let namespace = parts.next().unwrap_or("hara");
661 let name = parts.last().unwrap_or("interface");
662 format!("{namespace}:{name}")
663}
664
665fn valid_namespace(value: &str) -> bool {
666 let parts = value.split('.').collect::<Vec<_>>();
667 parts.len() > 1 && parts.iter().all(|part| valid_wit_name(part))
668}
669
670fn valid_wit_name(value: &str) -> bool {
671 !value.is_empty()
672 && value.chars().all(|character| {
673 character.is_ascii_alphanumeric() || character == '_' || character == '-'
674 })
675 && value
676 .chars()
677 .next()
678 .is_some_and(|character| character.is_ascii_lowercase() || character == '_')
679}
680
681fn valid_module(value: &str) -> bool {
682 !value.is_empty()
683 && value.ends_with(".wasm")
684 && !value.starts_with('/')
685 && !value.contains('\\')
686 && !value.contains(':')
687 && !value.bytes().any(|byte| byte == 0)
688 && value
689 .split('/')
690 .all(|part| !part.is_empty() && part != "." && part != "..")
691}
692
693pub(super) fn digest(bytes: &[u8]) -> String {
694 format!("sha256:{:x}", Sha256::digest(bytes))
695}
696
697pub(super) fn keyword(value: &str) -> Form {
698 Form::Keyword(value.into())
699}
700
701pub(super) fn symbol(value: &str) -> Form {
702 Form::Symbol(value.into())
703}
704
705pub(super) fn string(value: &str) -> Form {
706 Form::String(value.into())
707}
708
709pub(super) fn named_type(kind: &str, name: &str) -> Form {
710 Form::Vector(vec![keyword(kind), symbol(name)])
711}