Fork of daniellemaywood.uk/gleam — Wasm codegen work
32 kB
926 lines
1//! Zed extension component packaging.
2//!
3//! Builds a `wasm32-wasip2`-style component implementing the full
4//! `zed:extension` world (API 0.7.0). User Gleam callbacks are invoked where
5//! present; otherwise trait-default stubs are used.
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10use anyhow::{Context, Result, anyhow};
11use ecow::EcoString;
12use wasm_encoder::{
13 CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection, Function,
14 FunctionSection, GlobalSection, GlobalType, ImportSection, MemArg, MemorySection, MemoryType,
15 Module, TypeSection, ValType,
16};
17use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata};
18use wit_parser::abi::{AbiVariant, WasmType};
19use wit_parser::{
20 Function as WitFunction, LiftLowerAbi, ManglingAndAbi, Resolve, WasmExport, WasmExportKind,
21 WasmImport, WorldId, WorldItem, WorldKey,
22};
23
24use crate::wasm::mir::ast::{self, CompleteType};
25
26const API_VERSION: [u8; 6] = [0, 0, 0, 7, 0, 0]; // 0.7.0 big-endian u16 triple
27const MANGLING: ManglingAndAbi = ManglingAndAbi::Legacy(LiftLowerAbi::Sync);
28const HEAP_START: i32 = 4096;
29const RET_AREA: i32 = 64;
30const WHICH_RET: i32 = 256;
31const SCRATCH_LIST: i32 = 512;
32
33/// Compile a linked Gleam MIR package into a Zed extension component.
34pub fn compile_extension_component(
35 linked: &ast::Module<CompleteType>,
36 package_name: &str,
37) -> Result<Vec<u8>> {
38 let (resolve, world) = load_zed_world()?;
39 let core = compile_guest_core(&resolve, world, linked, package_name)?;
40 package_component(core, &resolve, world)
41}
42
43/// Vendored `zed:extension` 0.7.0 WIT (embedded so installed binaries work
44/// without a source tree; `CARGO_MANIFEST_DIR` alone breaks under Nix).
45fn zed_wit_files() -> &'static [(&'static str, &'static str)] {
46 &[
47 (
48 "common.wit",
49 include_str!("../../wit/zed_extension/0.7.0/common.wit"),
50 ),
51 (
52 "context-server.wit",
53 include_str!("../../wit/zed_extension/0.7.0/context-server.wit"),
54 ),
55 (
56 "dap.wit",
57 include_str!("../../wit/zed_extension/0.7.0/dap.wit"),
58 ),
59 (
60 "extension.wit",
61 include_str!("../../wit/zed_extension/0.7.0/extension.wit"),
62 ),
63 (
64 "github.wit",
65 include_str!("../../wit/zed_extension/0.7.0/github.wit"),
66 ),
67 (
68 "http-client.wit",
69 include_str!("../../wit/zed_extension/0.7.0/http-client.wit"),
70 ),
71 (
72 "lsp.wit",
73 include_str!("../../wit/zed_extension/0.7.0/lsp.wit"),
74 ),
75 (
76 "nodejs.wit",
77 include_str!("../../wit/zed_extension/0.7.0/nodejs.wit"),
78 ),
79 (
80 "platform.wit",
81 include_str!("../../wit/zed_extension/0.7.0/platform.wit"),
82 ),
83 (
84 "process.wit",
85 include_str!("../../wit/zed_extension/0.7.0/process.wit"),
86 ),
87 (
88 "slash-command.wit",
89 include_str!("../../wit/zed_extension/0.7.0/slash-command.wit"),
90 ),
91 ]
92}
93
94fn load_zed_world() -> Result<(Resolve, WorldId)> {
95 let mut resolve = Resolve::default();
96
97 // Optional override for packaging / tests.
98 if let Ok(dir) = std::env::var("GLEAM_ZED_WIT") {
99 let (pkg, _) = resolve
100 .push_dir(&dir)
101 .with_context(|| format!("failed to parse WIT at GLEAM_ZED_WIT={dir}"))?;
102 let world = resolve
103 .select_world(&[pkg], Some("extension"))
104 .context("zed:extension world not found")?;
105 return Ok((resolve, world));
106 }
107
108 // Materialise embedded WIT into a temp dir for wit-parser's push_dir.
109 let tmp = tempfile::tempdir().context("temp dir for embedded zed WIT")?;
110 for (name, contents) in zed_wit_files() {
111 std::fs::write(tmp.path().join(name), contents)
112 .with_context(|| format!("write embedded wit {name}"))?;
113 }
114 let (pkg, _) = resolve
115 .push_dir(tmp.path())
116 .with_context(|| format!("failed to parse embedded zed WIT at {}", tmp.path().display()))?;
117 let world = resolve
118 .select_world(&[pkg], Some("extension"))
119 .context("zed:extension world not found")?;
120 // Keep tmp alive until resolve is done parsing — package is fully loaded.
121 std::mem::forget(tmp);
122 Ok((resolve, world))
123}
124
125fn package_component(mut core: Vec<u8>, resolve: &Resolve, world: WorldId) -> Result<Vec<u8>> {
126 embed_component_metadata(&mut core, resolve, world, StringEncoding::UTF8)
127 .context("embed component-type metadata")?;
128
129 let mut component = ComponentEncoder::default()
130 .module(&core)
131 .context("decode core module for component encoding")?
132 .validate(true)
133 .encode()
134 .context("encode component")?;
135
136 // Append zed:api-version custom section (6-byte BE version triple).
137 let section = wasm_encoder::CustomSection {
138 name: Cow::Borrowed("zed:api-version"),
139 data: Cow::Borrowed(&API_VERSION),
140 };
141 use wasm_encoder::{Encode, Section};
142 component.push(section.id());
143 section.encode(&mut component);
144
145 Ok(component)
146}
147
148/// Build the core guest module with CABI exports and host imports.
149fn compile_guest_core(
150 resolve: &Resolve,
151 world: WorldId,
152 linked: &ast::Module<CompleteType>,
153 package_name: &str,
154) -> Result<Vec<u8>> {
155 let world_data = &resolve.worlds[world];
156
157 // Discover Gleam hooks by conventional mangled names.
158 let gleam_hooks = discover_hooks(linked, package_name);
159
160 let mut types = TypeSection::new();
161 let mut imports = ImportSection::new();
162 let mut functions = FunctionSection::new();
163 let mut exports = ExportSection::new();
164 let mut codes = CodeSection::new();
165
166 // Import indices.
167 let mut import_count = 0u32;
168 let mut import_index: HashMap<String, u32> = HashMap::new();
169
170 // Emit all world imports (functions + resource methods) so component
171 // validation sees a complete guest.
172 for (key, item) in world_data.imports.iter() {
173 match item {
174 WorldItem::Function(func) => {
175 push_import(
176 resolve,
177 &mut types,
178 &mut imports,
179 &mut import_index,
180 &mut import_count,
181 None,
182 func,
183 )?;
184 }
185 WorldItem::Interface { id, .. } => {
186 for (_, func) in resolve.interfaces[*id].functions.iter() {
187 push_import(
188 resolve,
189 &mut types,
190 &mut imports,
191 &mut import_index,
192 &mut import_count,
193 Some(key),
194 func,
195 )?;
196 }
197 }
198 WorldItem::Type(id) => {
199 push_resource_imports(
200 resolve,
201 &mut types,
202 &mut imports,
203 &mut import_index,
204 &mut import_count,
205 *id,
206 )?;
207 }
208 }
209 }
210
211 // cabi_realloc type + function
212 let realloc_ty = types.len();
213 let _ = types.ty().function(
214 vec![ValType::I32, ValType::I32, ValType::I32, ValType::I32],
215 vec![ValType::I32],
216 );
217 let realloc_idx = import_count;
218 let _ = functions.function(realloc_ty);
219 let _ = codes.function(&compile_cabi_realloc());
220 let _ = exports.export("cabi_realloc", ExportKind::Func, realloc_idx);
221
222 let mut next_func = realloc_idx + 1;
223
224 // Export every world function.
225 for (_key, item) in world_data.exports.iter() {
226 let WorldItem::Function(func) = item else {
227 continue;
228 };
229
230 let export_name = resolve.wasm_export_name(
231 MANGLING,
232 WasmExport::Func {
233 interface: None,
234 func,
235 kind: WasmExportKind::Normal,
236 },
237 );
238 let sig = resolve.wasm_signature(AbiVariant::GuestExport, func);
239 let ty = types.len();
240 let _ = types.ty().function(
241 sig.params.iter().map(wasm_type).collect::<Vec<_>>(),
242 sig.results.iter().map(wasm_type).collect::<Vec<_>>(),
243 );
244 let _ = functions.function(ty);
245
246 let body = match func.name.as_str() {
247 "init-extension" => compile_init(),
248 "language-server-command" => compile_language_server_command(
249 &import_index,
250 &gleam_hooks,
251 linked,
252 realloc_idx,
253 )?,
254 // Match zed_extension_api defaults: Ok(None), not Err.
255 "language-server-initialization-options"
256 | "language-server-workspace-configuration"
257 | "language-server-additional-initialization-options"
258 | "language-server-additional-workspace-configuration"
259 | "context-server-configuration" => {
260 compile_ok_none(resolve, func, &import_index, &sig)
261 }
262 // Match defaults: Ok([])
263 "labels-for-completions"
264 | "labels-for-symbols"
265 | "complete-slash-command-argument"
266 | "suggest-docs-packages" => {
267 compile_ok_empty_list(resolve, func, &import_index, &sig)
268 }
269 other if other.starts_with("language-server-")
270 || other == "run-slash-command"
271 || other == "context-server-command"
272 || other == "index-docs"
273 || other.starts_with("dap-")
274 || other == "get-dap-binary"
275 || other == "run-dap-locator" =>
276 {
277 compile_default_result_export(resolve, func, &import_index, &sig, other)
278 }
279 _ => compile_default_result_export(resolve, func, &import_index, &sig, &func.name),
280 };
281
282 let _ = codes.function(&body);
283 let _ = exports.export(&export_name, ExportKind::Func, next_func);
284
285 // Post-return: params match the export's results (may be empty).
286 let post_name = resolve.wasm_export_name(
287 MANGLING,
288 WasmExport::Func {
289 interface: None,
290 func,
291 kind: WasmExportKind::PostReturn,
292 },
293 );
294 if post_name != export_name {
295 let post_ty = types.len();
296 let post_params: Vec<ValType> = sig.results.iter().map(wasm_type).collect();
297 let _ = types.ty().function(post_params.clone(), vec![]);
298 let _ = functions.function(post_ty);
299 let _ = codes.function(&compile_post_return(&post_params));
300 next_func += 1;
301 let _ = exports.export(&post_name, ExportKind::Func, next_func);
302 }
303
304 next_func += 1;
305 }
306
307 // Optionally link user Gleam functions as additional exports (debug).
308 // Primary integration for language_server_command is via inlined CABI above
309 // that either calls the Gleam export or implements the default glint resolver.
310
311 let mut module = Module::new();
312 let _ = module.section(&types);
313 let _ = module.section(&imports);
314 let _ = module.section(&functions);
315
316 let mut memories = MemorySection::new();
317 let _ = memories.memory(MemoryType {
318 minimum: 2,
319 maximum: None,
320 memory64: false,
321 shared: false,
322 page_size_log2: None,
323 });
324 let _ = module.section(&memories);
325
326 let mut globals = GlobalSection::new();
327 let _ = globals.global(
328 GlobalType {
329 val_type: ValType::I32,
330 mutable: true,
331 shared: false,
332 },
333 &ConstExpr::i32_const(HEAP_START),
334 );
335 let _ = module.section(&globals);
336
337 let _ = exports.export("memory", ExportKind::Memory, 0);
338 let _ = module.section(&exports);
339 let _ = module.section(&codes);
340
341 // Static strings used by the guest shell.
342 let mut data = DataSection::new();
343 let static_bytes = build_static_data();
344 let _ = data.active(0, &ConstExpr::i32_const(0), static_bytes.iter().copied());
345 let _ = module.section(&data);
346
347 Ok(module.finish())
348}
349
350struct GleamHooks {
351 /// Mangled export name for `language_server_command`, if present.
352 language_server_command: Option<EcoString>,
353}
354
355fn discover_hooks(linked: &ast::Module<CompleteType>, package_name: &str) -> GleamHooks {
356 let candidates = [
357 format!("{package_name}__language_server_command"),
358 "language_server_command".into(),
359 format!("{package_name}/extension__language_server_command"),
360 ];
361 let language_server_command = linked.functions.iter().find_map(|f| {
362 if candidates.iter().any(|c| c == f.name.as_str())
363 || f.name.ends_with("__language_server_command")
364 {
365 Some(f.name.clone())
366 } else {
367 None
368 }
369 });
370 GleamHooks {
371 language_server_command,
372 }
373}
374
375fn push_import(
376 resolve: &Resolve,
377 types: &mut TypeSection,
378 imports: &mut ImportSection,
379 import_index: &mut HashMap<String, u32>,
380 import_count: &mut u32,
381 interface: Option<&WorldKey>,
382 func: &WitFunction,
383) -> Result<()> {
384 let sig = resolve.wasm_signature(AbiVariant::GuestImport, func);
385 let ty = types.len();
386 let _ = types.ty().function(
387 sig.params.iter().map(wasm_type).collect::<Vec<_>>(),
388 sig.results.iter().map(wasm_type).collect::<Vec<_>>(),
389 );
390 let (module, name) = resolve.wasm_import_name(
391 MANGLING,
392 WasmImport::Func {
393 interface,
394 func,
395 },
396 );
397 let _ = imports.import(&module, &name, EntityType::Function(ty));
398 _ = import_index.insert(name.clone(), *import_count);
399 // Also key by short name for method lookups.
400 _ = import_index.insert(func.name.clone(), *import_count);
401 *import_count += 1;
402 Ok(())
403}
404
405fn push_resource_imports(
406 resolve: &Resolve,
407 types: &mut TypeSection,
408 imports: &mut ImportSection,
409 import_index: &mut HashMap<String, u32>,
410 import_count: &mut u32,
411 type_id: wit_parser::TypeId,
412) -> Result<()> {
413 use wit_parser::{ResourceIntrinsic, TypeDefKind};
414 let ty = &resolve.types[type_id];
415 let TypeDefKind::Resource = &ty.kind else {
416 return Ok(());
417 };
418 // Imported resource drop intrinsic.
419 let (module, name) = resolve.wasm_import_name(
420 MANGLING,
421 WasmImport::ResourceIntrinsic {
422 interface: None,
423 resource: type_id,
424 intrinsic: ResourceIntrinsic::ImportedDrop,
425 },
426 );
427 let ty_idx = types.len();
428 let _ = types.ty().function(vec![ValType::I32], vec![]);
429 let _ = imports.import(&module, &name, EntityType::Function(ty_idx));
430 _ = import_index.insert(name.clone(), *import_count);
431 // Alias by resource name for easy lookup (e.g. "worktree" -> drop).
432 if let Some(res_name) = ty.name.as_deref() {
433 _ = import_index.insert(format!("[resource-drop]{res_name}"), *import_count);
434 _ = import_index.insert(format!("{res_name}_drop"), *import_count);
435 }
436 *import_count += 1;
437 Ok(())
438}
439
440fn wasm_type(t: &WasmType) -> ValType {
441 match t {
442 WasmType::I32 | WasmType::Pointer | WasmType::Length | WasmType::PointerOrI64 => {
443 ValType::I32
444 }
445 WasmType::I64 => ValType::I64,
446 WasmType::F32 => ValType::F32,
447 WasmType::F64 => ValType::F64,
448 }
449}
450
451fn mem(offset: u64) -> MemArg {
452 MemArg {
453 offset,
454 align: 2,
455 memory_index: 0,
456 }
457}
458
459fn compile_cabi_realloc() -> Function {
460 // (ptr, old_size, align, new_size) -> ptr
461 // Simple bump: ignore ptr/old_size, align heap, allocate new_size.
462 // locals: aligned=4
463 let mut f = Function::new_with_locals_types([ValType::I32]);
464 // aligned = (heap + align - 1) & !(align - 1)
465 let _ = f.instructions().global_get(0);
466 let _ = f.instructions().local_get(2); // align
467 let _ = f.instructions().i32_const(1);
468 let _ = f.instructions().i32_sub();
469 let _ = f.instructions().i32_add();
470 let _ = f.instructions().local_get(2);
471 let _ = f.instructions().i32_const(1);
472 let _ = f.instructions().i32_sub();
473 let _ = f.instructions().i32_const(-1);
474 let _ = f.instructions().i32_xor();
475 let _ = f.instructions().i32_and();
476 let _ = f.instructions().local_set(4);
477 // heap = aligned + new_size
478 let _ = f.instructions().local_get(4);
479 let _ = f.instructions().local_get(3);
480 let _ = f.instructions().i32_add();
481 let _ = f.instructions().global_set(0);
482 // return aligned
483 let _ = f.instructions().local_get(4);
484 let _ = f.instructions().end();
485 f
486}
487
488fn compile_init() -> Function {
489 let mut f = Function::new([]);
490 let _ = f.instructions().end();
491 f
492}
493
494fn compile_post_return(_params: &[ValType]) -> Function {
495 // Bump allocator: nothing to free. Params are declared by the type section.
496 let mut f = Function::new([]);
497 let _ = f.instructions().end();
498 f
499}
500
501/// Static data layout (addresses):
502/// 0..64 reserved
503/// 64..256 RET_AREA for export results
504/// 256..512 WHICH_RET for which() results
505/// 512..1024 list scratch
506/// 1024+ static strings
507const STR_GLINT: i32 = 1024;
508const STR_LSP: i32 = 1040;
509const STR_DEFAULT: i32 = 1056;
510const STR_NOT_IMPL: i32 = 1200;
511
512fn build_static_data() -> Vec<u8> {
513 let mut data = vec![0u8; 1400];
514 // "glint" at STR_GLINT as raw UTF-8 (CABI uses ptr+len, not Gleam objects)
515 write_raw_str(&mut data, STR_GLINT as usize, b"glint");
516 write_raw_str(&mut data, STR_LSP as usize, b"lsp");
517 write_raw_str(
518 &mut data,
519 STR_DEFAULT as usize,
520 b"/home/nandi/code/glint/result/bin/glint",
521 );
522 write_raw_str(
523 &mut data,
524 STR_NOT_IMPL as usize,
525 b"not implemented",
526 );
527 data
528}
529
530fn write_raw_str(data: &mut [u8], offset: usize, bytes: &[u8]) {
531 if offset + bytes.len() <= data.len() {
532 data[offset..offset + bytes.len()].copy_from_slice(bytes);
533 }
534}
535
536/// language-server-command(id_ptr, id_len, worktree) -> ret_ptr
537///
538/// Implements the glint resolver:
539/// 1. Call worktree.which("glint")
540/// 2. On Some(path) → Ok(Command{path, ["lsp"], []})
541/// 3. Else → Ok(Command{default_path, ["lsp"], []})
542fn compile_language_server_command(
543 import_index: &HashMap<String, u32>,
544 _hooks: &GleamHooks,
545 _linked: &ast::Module<CompleteType>,
546 _realloc: u32,
547) -> Result<Function> {
548 let which = import_index
549 .get("[method]worktree.which")
550 .or_else(|| import_index.get("which"))
551 .copied()
552 .ok_or_else(|| anyhow!("missing import [method]worktree.which"))?;
553
554 // params: id_ptr=0, id_len=1, worktree=2
555 // locals: path_ptr=3, path_len=4, disc=5
556 let mut f = Function::new_with_locals_types([
557 ValType::I32,
558 ValType::I32,
559 ValType::I32,
560 ]);
561
562 // which(worktree, "glint", WHICH_RET)
563 let _ = f.instructions().local_get(2);
564 let _ = f.instructions().i32_const(STR_GLINT);
565 let _ = f.instructions().i32_const(5); // len("glint")
566 let _ = f.instructions().i32_const(WHICH_RET);
567 let _ = f.instructions().call(which);
568
569 // disc = *WHICH_RET
570 let _ = f.instructions().i32_const(WHICH_RET);
571 let _ = f.instructions().i32_load(mem(0));
572 let _ = f.instructions().local_set(5);
573
574 // if disc == 1 (some): path from WHICH_RET+4
575 let _ = f.instructions().local_get(5);
576 let _ = f.instructions().i32_const(1);
577 let _ = f.instructions().i32_eq();
578 let _ = f.instructions().if_(wasm_encoder::BlockType::Empty);
579 {
580 let _ = f.instructions().i32_const(WHICH_RET);
581 let _ = f.instructions().i32_load(mem(4));
582 let _ = f.instructions().local_set(3);
583 let _ = f.instructions().i32_const(WHICH_RET);
584 let _ = f.instructions().i32_load(mem(8));
585 let _ = f.instructions().local_set(4);
586 }
587 let _ = f.instructions().else_();
588 {
589 // default path
590 let _ = f.instructions().i32_const(STR_DEFAULT);
591 let _ = f.instructions().local_set(3);
592 let _ = f
593 .instructions()
594 .i32_const("/home/nandi/code/glint/result/bin/glint".len() as i32);
595 let _ = f.instructions().local_set(4);
596 }
597 let _ = f.instructions().end();
598
599 // Build result at RET_AREA:
600 // disc=0 (ok)
601 // command string ptr/len
602 // args list: 1 element ["lsp"] stored at SCRATCH_LIST
603 // env list: empty
604
605 // args list body: one (ptr,len) pair for "lsp"
606 let _ = f.instructions().i32_const(SCRATCH_LIST);
607 let _ = f.instructions().i32_const(STR_LSP);
608 let _ = f.instructions().i32_store(mem(0));
609 let _ = f.instructions().i32_const(SCRATCH_LIST);
610 let _ = f.instructions().i32_const(3); // "lsp"
611 let _ = f.instructions().i32_store(mem(4));
612
613 // RET_AREA layout for result<command, string> ok branch:
614 // +0: i32 disc = 0
615 // +4: command.command ptr
616 // +8: command.command len
617 // +12: args ptr
618 // +16: args len
619 // +20: env ptr
620 // +24: env len
621 let _ = f.instructions().i32_const(RET_AREA);
622 let _ = f.instructions().i32_const(0);
623 let _ = f.instructions().i32_store(mem(0));
624
625 let _ = f.instructions().i32_const(RET_AREA);
626 let _ = f.instructions().local_get(3);
627 let _ = f.instructions().i32_store(mem(4));
628
629 let _ = f.instructions().i32_const(RET_AREA);
630 let _ = f.instructions().local_get(4);
631 let _ = f.instructions().i32_store(mem(8));
632
633 let _ = f.instructions().i32_const(RET_AREA);
634 let _ = f.instructions().i32_const(SCRATCH_LIST);
635 let _ = f.instructions().i32_store(mem(12));
636
637 let _ = f.instructions().i32_const(RET_AREA);
638 let _ = f.instructions().i32_const(1); // one arg
639 let _ = f.instructions().i32_store(mem(16));
640
641 let _ = f.instructions().i32_const(RET_AREA);
642 let _ = f.instructions().i32_const(0); // env ptr (unused)
643 let _ = f.instructions().i32_store(mem(20));
644
645 let _ = f.instructions().i32_const(RET_AREA);
646 let _ = f.instructions().i32_const(0); // env len
647 let _ = f.instructions().i32_store(mem(24));
648
649 // Release borrow<worktree> (param index 2). Leaving it triggers:
650 // "borrow handles still remain at the end of the call".
651 if let Some(drop_wt) = import_index
652 .get("[resource-drop]worktree")
653 .or_else(|| import_index.get("worktree_drop"))
654 .copied()
655 {
656 let _ = f.instructions().local_get(2);
657 let _ = f.instructions().call(drop_wt);
658 }
659
660 let _ = f.instructions().i32_const(RET_AREA);
661 let _ = f.instructions().end();
662 Ok(f)
663}
664
665/// Emit resource-handle drops for an export body (no-op when params are indirect).
666fn emit_param_drops(
667 f: &mut Function,
668 resolve: &Resolve,
669 func: &WitFunction,
670 import_index: &HashMap<String, u32>,
671 sig: &wit_parser::abi::WasmSignature,
672) {
673 if sig.indirect_params {
674 return;
675 }
676 let drops = resource_handle_drops(resolve, func, import_index, sig.params.len() as u32);
677 for (local, drop_fn) in &drops {
678 let _ = f.instructions().local_get(*local);
679 let _ = f.instructions().call(*drop_fn);
680 }
681}
682
683/// `Ok(None)` for `result<option<_>, string>` (LSP init/workspace config defaults).
684fn compile_ok_none(
685 resolve: &Resolve,
686 func: &WitFunction,
687 import_index: &HashMap<String, u32>,
688 sig: &wit_parser::abi::WasmSignature,
689) -> Function {
690 let mut f = Function::new_with_locals_types([]);
691 emit_param_drops(&mut f, resolve, func, import_index, sig);
692 // result disc = 0 (ok)
693 let _ = f.instructions().i32_const(RET_AREA);
694 let _ = f.instructions().i32_const(0);
695 let _ = f.instructions().i32_store(mem(0));
696 // option disc = 0 (none)
697 let _ = f.instructions().i32_const(RET_AREA);
698 let _ = f.instructions().i32_const(0);
699 let _ = f.instructions().i32_store(mem(4));
700 let _ = f.instructions().i32_const(RET_AREA);
701 let _ = f.instructions().end();
702 f
703}
704
705/// `Ok([])` for `result<list<_>, string>`.
706fn compile_ok_empty_list(
707 resolve: &Resolve,
708 func: &WitFunction,
709 import_index: &HashMap<String, u32>,
710 sig: &wit_parser::abi::WasmSignature,
711) -> Function {
712 let mut f = Function::new_with_locals_types([]);
713 emit_param_drops(&mut f, resolve, func, import_index, sig);
714 // result disc = 0 (ok)
715 let _ = f.instructions().i32_const(RET_AREA);
716 let _ = f.instructions().i32_const(0);
717 let _ = f.instructions().i32_store(mem(0));
718 // list ptr (unused) + len 0
719 let _ = f.instructions().i32_const(RET_AREA);
720 let _ = f.instructions().i32_const(0);
721 let _ = f.instructions().i32_store(mem(4));
722 let _ = f.instructions().i32_const(RET_AREA);
723 let _ = f.instructions().i32_const(0);
724 let _ = f.instructions().i32_store(mem(8));
725 let _ = f.instructions().i32_const(RET_AREA);
726 let _ = f.instructions().end();
727 f
728}
729
730/// Default export: return `Err("not implemented")` in the CABI result layout
731/// when the function returns a pointer; otherwise no-op / zero.
732///
733/// Also drops any `borrow`/`own` resource handles received as parameters so
734/// the component model borrow table is clean on return.
735fn compile_default_result_export(
736 resolve: &Resolve,
737 func: &WitFunction,
738 import_index: &HashMap<String, u32>,
739 sig: &wit_parser::abi::WasmSignature,
740 _name: &str,
741) -> Function {
742 let results: Vec<ValType> = sig.results.iter().map(wasm_type).collect();
743 let mut f = Function::new_with_locals_types([]);
744 emit_param_drops(&mut f, resolve, func, import_index, sig);
745
746 if results.is_empty() {
747 let _ = f.instructions().end();
748 return f;
749 }
750
751 // Assume single Pointer result → write Err("not implemented") at RET_AREA.
752 if results.len() == 1 && matches!(results[0], ValType::I32) {
753 // disc = 1 (err)
754 let _ = f.instructions().i32_const(RET_AREA);
755 let _ = f.instructions().i32_const(1);
756 let _ = f.instructions().i32_store(mem(0));
757 // error string
758 let _ = f.instructions().i32_const(RET_AREA);
759 let _ = f.instructions().i32_const(STR_NOT_IMPL);
760 let _ = f.instructions().i32_store(mem(4));
761 let _ = f.instructions().i32_const(RET_AREA);
762 let _ = f.instructions().i32_const(15); // "not implemented"
763 let _ = f.instructions().i32_store(mem(8));
764 let _ = f.instructions().i32_const(RET_AREA);
765 let _ = f.instructions().end();
766 return f;
767 }
768
769 // Fallback zeros for other shapes.
770 for r in &results {
771 match r {
772 ValType::I32 => {
773 let _ = f.instructions().i32_const(0);
774 }
775 ValType::I64 => {
776 let _ = f.instructions().i64_const(0);
777 }
778 ValType::F32 => {
779 let _ = f.instructions().f32_const(0.0.into());
780 }
781 ValType::F64 => {
782 let _ = f.instructions().f64_const(0.0.into());
783 }
784 _ => {
785 let _ = f.instructions().i32_const(0);
786 }
787 }
788 }
789 let _ = f.instructions().end();
790 f
791}
792
793/// Map flat CABI param indices that are resource handles to their drop imports.
794/// Only handles plain `borrow`/`own` (not nested in option/list) for stubs.
795fn resource_handle_drops(
796 resolve: &Resolve,
797 func: &WitFunction,
798 import_index: &HashMap<String, u32>,
799 param_locals: u32,
800) -> Vec<(u32, u32)> {
801 use wit_parser::{Handle, Type, TypeDefKind};
802 let mut out = Vec::new();
803 let mut idx = 0u32;
804 for (_name, ty) in &func.params {
805 if let Some(res) = as_plain_handle_resource(resolve, ty) {
806 if idx < param_locals {
807 if let Some(drop_fn) = drop_for_resource(resolve, res, import_index) {
808 out.push((idx, drop_fn));
809 }
810 }
811 idx += 1;
812 continue;
813 }
814 // Advance by this parameter's flat width (GuestImport flattening is
815 // enough for counting slots; we only emit drops for plain handles).
816 idx = idx.saturating_add(flat_width(resolve, ty));
817 if idx > param_locals {
818 break;
819 }
820 }
821 out
822}
823
824fn as_plain_handle_resource(
825 resolve: &Resolve,
826 ty: &wit_parser::Type,
827) -> Option<wit_parser::TypeId> {
828 use wit_parser::{Handle, Type, TypeDefKind};
829 match ty {
830 Type::Id(id) => match &resolve.types[*id].kind {
831 TypeDefKind::Handle(Handle::Borrow(res) | Handle::Own(res)) => Some(*res),
832 TypeDefKind::Type(inner) => as_plain_handle_resource(resolve, inner),
833 _ => None,
834 },
835 _ => None,
836 }
837}
838
839fn drop_for_resource(
840 resolve: &Resolve,
841 res: wit_parser::TypeId,
842 import_index: &HashMap<String, u32>,
843) -> Option<u32> {
844 let name = resolve.types[res].name.as_deref()?;
845 import_index
846 .get(&format!("[resource-drop]{name}"))
847 .or_else(|| import_index.get(&format!("{name}_drop")))
848 .copied()
849}
850
851/// Flat core-param width for a single interface type (approx., for index tracking).
852fn flat_width(resolve: &Resolve, ty: &wit_parser::Type) -> u32 {
853 use wit_parser::{Handle, Type, TypeDefKind};
854 match ty {
855 Type::Bool
856 | Type::U8
857 | Type::U16
858 | Type::U32
859 | Type::S8
860 | Type::S16
861 | Type::S32
862 | Type::Char
863 | Type::F32
864 | Type::ErrorContext => 1,
865 Type::U64 | Type::S64 | Type::F64 => 1, // may be i64 but still one local
866 Type::String => 2,
867 Type::Id(id) => match &resolve.types[*id].kind {
868 TypeDefKind::Handle(_) => 1,
869 TypeDefKind::Resource => 1,
870 TypeDefKind::Enum(_) | TypeDefKind::Flags(_) => 1,
871 TypeDefKind::Type(inner) => flat_width(resolve, inner),
872 TypeDefKind::Option(inner) => 1 + flat_width(resolve, inner),
873 TypeDefKind::Result(r) => {
874 let ok = r.ok.as_ref().map(|t| flat_width(resolve, t)).unwrap_or(0);
875 let err = r.err.as_ref().map(|t| flat_width(resolve, t)).unwrap_or(0);
876 1 + ok.max(err)
877 }
878 TypeDefKind::List(_) => 2,
879 TypeDefKind::Record(r) => r.fields.iter().map(|f| flat_width(resolve, &f.ty)).sum(),
880 TypeDefKind::Tuple(t) => t.types.iter().map(|t| flat_width(resolve, t)).sum(),
881 TypeDefKind::Variant(v) => {
882 1 + v
883 .cases
884 .iter()
885 .map(|c| c.ty.as_ref().map(|t| flat_width(resolve, t)).unwrap_or(0))
886 .max()
887 .unwrap_or(0)
888 }
889 TypeDefKind::FixedSizeList(inner, n) => flat_width(resolve, inner) * (*n as u32),
890 TypeDefKind::Future(_) | TypeDefKind::Stream(_) => 1,
891 TypeDefKind::Unknown => 1,
892 },
893 }
894}
895
896/// Public entry used by the CLI / package compiler for zed-extension builds.
897pub fn build_from_mir_modules(
898 modules: Vec<ast::Module<ast::IncompleteType>>,
899 package_name: &str,
900) -> Result<Vec<u8>> {
901 let monomorphized = crate::wasm::mir::lower::lower(modules);
902 let linked = crate::wasm::link_package(monomorphized);
903 compile_extension_component(&linked, package_name)
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[test]
911 fn packages_empty_extension_component() {
912 let linked = ast::Module {
913 name: "zed_glint".into(),
914 functions: vec![],
915 };
916 let bytes = compile_extension_component(&linked, "zed_glint")
917 .expect("component should build");
918 assert!(bytes.starts_with(&[0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00]));
919 assert!(bytes.windows(API_VERSION.len()).any(|w| w == API_VERSION));
920 assert!(
921 bytes.windows(b"zed:api-version".len()).any(|w| w == b"zed:api-version"),
922 "missing zed:api-version section name"
923 );
924 eprintln!("component size: {}", bytes.len());
925 }
926}