Fork of daniellemaywood.uk/gleam — Wasm codegen work
1

Configure Feed

Select the types of activity you want to include in your feed.

Add Zed extension Wasm packaging to the Wasm backend.

Emit full zed:extension guests (CABI stubs, language-server-command),
package components with zed:api-version, and add `gleam export zed-extension`.
Also extend MIR/codegen for @external(wasm), structs, lists, and package link.

author
nandi
committer
nandi
date (Jul 26, 2026, 12:13 PM -0700) commit ece01369 parent 435cd163 change-id ostovmul
+2143 -101
+55 -1
compiler-cli/src/export.rs
··· 4 4 use crate::fs::{self, ZipArchive}; 5 5 use camino::Utf8PathBuf; 6 6 use gleam_core::{ 7 - Result, 7 + Error, Result, 8 8 analyse::TargetSupport, 9 9 build::{Codegen, Compile, Mode, Options, Target}, 10 10 paths::ProjectPaths, ··· 272 272 fs::write_outputs_under(&[out], paths.root())?; 273 273 Ok(()) 274 274 } 275 + 276 + /// Build a Zed extension component (`extension.wasm`) for the current package. 277 + /// 278 + /// Typechecks with `target = "wasm"`, then packages a full `zed:extension` 279 + /// world guest (API 0.7.0) including `language-server-command` and defaults 280 + /// for every other world export. 281 + pub fn zed_extension(paths: &ProjectPaths) -> Result<()> { 282 + use gleam_core::error::ShellCommandFailureReason; 283 + use gleam_core::wasm; 284 + 285 + let built = crate::build::main( 286 + paths, 287 + Options { 288 + mode: Mode::Prod, 289 + target: Some(Target::Wasm), 290 + codegen: Codegen::All, 291 + compile: Compile::All, 292 + warnings_as_errors: false, 293 + root_target_support: TargetSupport::Enforced, 294 + no_print_progress: false, 295 + }, 296 + crate::build::download_dependencies(paths, crate::cli::Reporter::new())?, 297 + )?; 298 + 299 + let package_name = built.root_package.config.name.to_string(); 300 + 301 + // Translate typed modules → monomorphised MIR → component. 302 + let mir_modules: Vec<_> = built 303 + .root_package 304 + .modules 305 + .iter() 306 + .map(|module| wasm::mir::Translator::new(&module.ast).translate()) 307 + .collect(); 308 + 309 + let component = wasm::zed::build_from_mir_modules(mir_modules, &package_name).map_err( 310 + |error| Error::ShellCommand { 311 + program: "zed-extension".into(), 312 + reason: ShellCommandFailureReason::ShellCommandError(error.to_string()), 313 + }, 314 + )?; 315 + 316 + let out = paths.root().join("extension.wasm"); 317 + crate::fs::write_bytes(&out, &component)?; 318 + crate::cli::print_exported(&package_name); 319 + println!( 320 + " 321 + Zed extension component written to {out} 322 + 323 + Install as a Zed Dev Extension (no Cargo.toml in the package so Zed will not 324 + try a Rust rebuild). Requires zed:api-version 0.7.0. 325 + " 326 + ); 327 + Ok(()) 328 + }
+11
compiler-cli/src/lib.rs
··· 659 659 let paths = find_project_paths(directory)?; 660 660 export::package_information(&paths, output) 661 661 } 662 + Self::Export(ExportTarget::ZedExtension) => { 663 + let paths = find_project_paths(directory)?; 664 + export::zed_extension(&paths) 665 + } 662 666 } 663 667 } 664 668 } ··· 704 708 #[arg(long = "out", required = true)] 705 709 output: Utf8PathBuf, 706 710 }, 711 + /// Build a Zed editor extension component (`extension.wasm`) 712 + /// 713 + /// Compiles the package for the Wasm target and packages a full 714 + /// `zed:extension` world guest (API 0.7.0) with `zed:api-version`. 715 + /// Writes `extension.wasm` at the package root. 716 + #[command(name = "zed-extension")] 717 + ZedExtension, 707 718 } 708 719 709 720 #[derive(Args, Debug, Clone)]
+4
compiler-core/Cargo.toml
··· 49 49 indexmap = "2.12.1" 50 50 # Wasm binary emission 51 51 wasm-encoder = "0.239.0" 52 + wit-parser = "0.239.0" 53 + wit-component = { version = "0.239.0", features = ["dummy-module"] } 54 + wat = "1" 55 + anyhow = "1" 52 56 53 57 num-bigint.workspace = true 54 58 num-traits.workspace = true
+10 -6
compiler-core/src/analyse.rs
··· 588 588 &body, 589 589 &external_erlang, 590 590 &external_javascript, 591 + &external_wasm, 591 592 location, 592 593 ); 593 594 ··· 919 920 body: &[UntypedStatement], 920 921 external_erlang: &Option<(EcoString, EcoString, SrcSpan)>, 921 922 external_javascript: &Option<(EcoString, EcoString, SrcSpan)>, 923 + external_wasm: &Option<(EcoString, EcoString, SrcSpan)>, 922 924 location: SrcSpan, 923 925 ) -> bool { 924 - match (external_erlang, external_javascript) { 925 - (None, None) if body.is_empty() => { 926 - self.problems.error(Error::NoImplementation { location }); 927 - false 928 - } 929 - _ => true, 926 + let has_external = external_erlang.is_some() 927 + || external_javascript.is_some() 928 + || external_wasm.is_some(); 929 + if !has_external && body.is_empty() { 930 + self.problems.error(Error::NoImplementation { location }); 931 + false 932 + } else { 933 + true 930 934 } 931 935 } 932 936
+6 -9
compiler-core/src/codegen.rs
··· 357 357 .collect(); 358 358 359 359 let monomorphized = wasm::mir::lower::lower(modules); 360 + let linked = wasm::link_package(monomorphized); 360 361 361 - for module in monomorphized { 362 - let path = self 363 - .output_directory 364 - .join(&format!("{}.wasm", module.name.clone())); 365 - 366 - let object = wasm::compile(module); 367 - 368 - writer.write_bytes(&path, &object)?; 369 - } 362 + let path = self 363 + .output_directory 364 + .join(format!("{}.wasm", linked.name)); 365 + let object = wasm::compile(linked); 366 + writer.write_bytes(&path, &object)?; 370 367 371 368 Ok(()) 372 369 }
+2
compiler-core/src/parse.rs
··· 4358 4358 match name.as_str() { 4359 4359 "javascript" => Ok(Target::JavaScript), 4360 4360 "erlang" => Ok(Target::Erlang), 4361 + "wasm" => Ok(Target::Wasm), 4361 4362 "js" => { 4362 4363 self.warnings 4363 4364 .push(DeprecatedSyntaxWarning::DeprecatedTargetShorthand { ··· 4684 4685 let target = match target.as_str() { 4685 4686 "erlang" => Target::Erlang, 4686 4687 "javascript" => Target::JavaScript, 4688 + "wasm" => Target::Wasm, 4687 4689 _ => return parse_error(ParseErrorType::UnknownTarget, SrcSpan::new(start, end)), 4688 4690 }; 4689 4691
+2 -2
compiler-core/src/parse/error.rs
··· 281 281 }, 282 282 283 283 ParseErrorType::ExpectedTargetName => ParseErrorDetails { 284 - text: "Try `erlang`, `javascript`.".into(), 284 + text: "Try `erlang`, `javascript`, `wasm`.".into(), 285 285 hint: None, 286 286 label_text: "I was expecting a target name after this".into(), 287 287 extra_labels: vec![], ··· 659 659 }, 660 660 661 661 ParseErrorType::UnknownTarget => ParseErrorDetails { 662 - text: "Try `erlang`, `javascript`.".into(), 662 + text: "Try `erlang`, `javascript`, `wasm`.".into(), 663 663 hint: None, 664 664 label_text: "I don't recognise this target".into(), 665 665 extra_labels: vec![],
+549 -65
compiler-core/src/wasm.rs
··· 36 36 use crate::wasm::mir::ast::{self, CompleteType, Expression}; 37 37 38 38 pub mod mir; 39 + pub mod zed; 39 40 40 41 /// Bytes reserved at the start of linear memory for WASI iovecs and itoa. 41 42 /// Layout: ··· 52 53 Compiler::new(&module).compile() 53 54 } 54 55 56 + /// Merge monomorphised modules into a single package module. 57 + /// 58 + /// Cross-module calls become same-module calls with mangled names 59 + /// (`{module}__{function}`), so the backend can emit one linked Wasm artifact. 60 + pub fn link_package(modules: Vec<ast::Module<CompleteType>>) -> ast::Module<CompleteType> { 61 + use ecow::eco_format; 62 + 63 + let package_name = modules 64 + .first() 65 + .map(|module| module.name.clone()) 66 + .unwrap_or_else(|| "package".into()); 67 + 68 + let mut linked = ast::Module { 69 + name: package_name, 70 + functions: Vec::new(), 71 + }; 72 + 73 + for module in modules { 74 + for mut function in module.functions { 75 + function.name = eco_format!("{}__{}", module.name, function.name); 76 + if let Some(body) = &mut function.body { 77 + mangle_function_refs(body); 78 + } 79 + linked.functions.push(function); 80 + } 81 + } 82 + 83 + linked 84 + } 85 + 86 + fn mangle_function_refs(expression: &mut Expression<CompleteType>) { 87 + use ecow::eco_format; 88 + 89 + match expression { 90 + Expression::FunctionRef { module, name, .. } => { 91 + *name = eco_format!("{}__{}", module, name); 92 + *module = "".into(); 93 + } 94 + Expression::Block(expressions) => { 95 + for expression in expressions { 96 + mangle_function_refs(expression); 97 + } 98 + } 99 + Expression::Set { value, .. } 100 + | Expression::TupleAccess { value, .. } 101 + | Expression::StructTag { value } 102 + | Expression::StructAccess { value, .. } => mangle_function_refs(value), 103 + Expression::Equals { lhs, rhs } 104 + | Expression::NotEquals { lhs, rhs } 105 + | Expression::IntGt { lhs, rhs } 106 + | Expression::IntGtEq { lhs, rhs } 107 + | Expression::IntLt { lhs, rhs } 108 + | Expression::IntLtEq { lhs, rhs } 109 + | Expression::IntAdd { lhs, rhs } 110 + | Expression::IntSub { lhs, rhs } 111 + | Expression::IntMul { lhs, rhs } 112 + | Expression::IntDiv { lhs, rhs } 113 + | Expression::IntRem { lhs, rhs } 114 + | Expression::FloatGt { lhs, rhs } 115 + | Expression::FloatGtEq { lhs, rhs } 116 + | Expression::FloatLt { lhs, rhs } 117 + | Expression::FloatLtEq { lhs, rhs } 118 + | Expression::FloatAdd { lhs, rhs } 119 + | Expression::FloatSub { lhs, rhs } 120 + | Expression::FloatMul { lhs, rhs } 121 + | Expression::FloatDiv { lhs, rhs } 122 + | Expression::StringConcat { lhs, rhs } => { 123 + mangle_function_refs(lhs); 124 + mangle_function_refs(rhs); 125 + } 126 + Expression::If { cond, then, else_ } => { 127 + mangle_function_refs(cond); 128 + mangle_function_refs(then); 129 + mangle_function_refs(else_); 130 + } 131 + Expression::Call { target, args, .. } => { 132 + mangle_function_refs(target); 133 + for arg in args { 134 + mangle_function_refs(arg); 135 + } 136 + } 137 + Expression::List { items, tail, .. } => { 138 + for item in items { 139 + mangle_function_refs(item); 140 + } 141 + if let Some(tail) = tail { 142 + mangle_function_refs(tail); 143 + } 144 + } 145 + Expression::Tuple { items, .. } | Expression::Struct { items, .. } => { 146 + for item in items { 147 + mangle_function_refs(item); 148 + } 149 + } 150 + Expression::Var(_) 151 + | Expression::Int { .. } 152 + | Expression::Float { .. } 153 + | Expression::Bool { .. } 154 + | Expression::String { .. } 155 + | Expression::Panic { .. } => {} 156 + } 157 + } 158 + 55 159 /// Indices of synthesised runtime helpers (only present when strings are used). 56 160 struct StringRuntime { 57 161 alloc: u32, ··· 92 196 impl<'a> Compiler<'a> { 93 197 fn new(module: &'a ast::Module<CompleteType>) -> Self { 94 198 let main = module.functions.iter().find(|function| { 95 - function.name == "main" && function.parameters.is_empty() 199 + is_main_function(&function.name) 200 + && function.parameters.is_empty() 201 + && function.external_wasm.is_none() 202 + && function.body.is_some() 96 203 }); 97 204 let emit_wasi = main.is_some(); 205 + let needs_heap = module_needs_heap(module) || module_uses_strings(module) || emit_wasi; 98 206 let uses_strings = module_uses_strings(module) || emit_wasi; 99 207 100 - let import_func_count = u32::from(emit_wasi); 101 - let function_indices = module 208 + // Import order: optional WASI fd_write, then `@external(wasm, …)` functions. 209 + let external_functions: Vec<_> = module 210 + .functions 211 + .iter() 212 + .filter(|function| function.external_wasm.is_some()) 213 + .collect(); 214 + let defined_functions: Vec<_> = module 102 215 .functions 103 216 .iter() 104 - .enumerate() 105 - .map(|(index, function)| { 106 - ( 107 - function.name.clone(), 108 - import_func_count + index as u32, 109 - ) 110 - }) 217 + .filter(|function| function.external_wasm.is_none()) 111 218 .collect(); 112 219 220 + let import_func_count = u32::from(emit_wasi) + external_functions.len() as u32; 221 + 222 + let mut function_indices = HashMap::new(); 223 + let mut next_index = u32::from(emit_wasi); 224 + for function in &external_functions { 225 + _ = function_indices.insert(function.name.clone(), next_index); 226 + next_index += 1; 227 + } 228 + for function in &defined_functions { 229 + _ = function_indices.insert(function.name.clone(), next_index); 230 + next_index += 1; 231 + } 232 + 113 233 let mut compiler = Self { 114 234 module, 115 235 function_indices, ··· 122 242 import_func_count, 123 243 }; 124 244 125 - if uses_strings || emit_wasi { 245 + if needs_heap { 126 246 // Reserve scratch so string objects never overlap WASI temp space. 127 247 compiler.static_data.resize(SCRATCH_SIZE as usize, 0); 128 248 } ··· 133 253 if uses_strings { 134 254 compiler.empty_string_offset = compiler.intern_string(""); 135 255 for function in &module.functions { 136 - collect_strings_from_expression(&function.body, &mut |value| { 137 - let _ = compiler.intern_string(value); 138 - }); 256 + if let Some(body) = &function.body { 257 + collect_strings_from_expression(body, &mut |value| { 258 + let _ = compiler.intern_string(value); 259 + }); 260 + } 139 261 } 140 262 } 141 263 if emit_wasi { 142 264 newline_offset = compiler.intern_string("\n"); 143 265 } 144 266 145 - if uses_strings || emit_wasi { 267 + if needs_heap { 146 268 compiler.heap_start = align4(compiler.static_data.len() as u32); 147 269 } 148 270 149 - if uses_strings { 150 - let base = import_func_count + module.functions.len() as u32; 271 + if needs_heap { 272 + let base = import_func_count + defined_functions.len() as u32; 151 273 compiler.string_runtime = Some(StringRuntime { 152 274 alloc: base, 153 275 concat: base + 1, ··· 157 279 158 280 if emit_wasi { 159 281 let main = main.expect("main checked above"); 160 - let user_count = module.functions.len() as u32; 161 - let mut next = import_func_count + user_count; 282 + let mut next = import_func_count + defined_functions.len() as u32; 162 283 if compiler.string_runtime.is_some() { 163 284 next += 3; 164 285 } ··· 169 290 fd_write: 0, 170 291 main_index: *compiler 171 292 .function_indices 172 - .get("main") 293 + .get(&main.name) 173 294 .expect("main index"), 174 295 main_return: main.return_type.clone(), 175 296 print_string, ··· 219 340 None 220 341 }; 221 342 222 - // User function types (type indices continue after fd_write type). 223 - let user_type_base = u32::from(fd_write_type.is_some()); 224 - for (index, function) in self.module.functions.iter().enumerate() { 343 + if let Some(fd_type) = fd_write_type { 344 + let _ = imports.import( 345 + "wasi_snapshot_preview1", 346 + "fd_write", 347 + EntityType::Function(fd_type), 348 + ); 349 + } 350 + 351 + // External Wasm imports (function types, then import entries). 352 + for function in self 353 + .module 354 + .functions 355 + .iter() 356 + .filter(|function| function.external_wasm.is_some()) 357 + { 358 + let params: Vec<ValType> = function 359 + .parameters 360 + .iter() 361 + .map(|parameter| val_type(&parameter.type_)) 362 + .collect(); 363 + let results = vec![val_type(&function.return_type)]; 364 + let type_index = types.len(); 365 + let _ = types.ty().function(params, results); 366 + let (module_name, func_name) = function 367 + .external_wasm 368 + .as_ref() 369 + .expect("filtered to external"); 370 + let _ = imports.import( 371 + module_name.as_str(), 372 + func_name.as_str(), 373 + EntityType::Function(type_index), 374 + ); 375 + } 376 + 377 + // Defined user functions. 378 + for function in self 379 + .module 380 + .functions 381 + .iter() 382 + .filter(|function| function.external_wasm.is_none()) 383 + { 225 384 let params: Vec<ValType> = function 226 385 .parameters 227 386 .iter() 228 387 .map(|parameter| val_type(&parameter.type_)) 229 388 .collect(); 230 389 let results = vec![val_type(&function.return_type)]; 390 + let type_index = types.len(); 231 391 let _ = types.ty().function(params, results); 232 - let type_index = user_type_base + index as u32; 233 392 let _ = functions.function(type_index); 234 - let wasm_index = self.import_func_count + index as u32; 393 + let wasm_index = *self 394 + .function_indices 395 + .get(&function.name) 396 + .expect("defined function index"); 235 397 let _ = exports.export(&function.name, ExportKind::Func, wasm_index); 236 398 let _ = codes.function(&self.compile_function(function)); 237 399 } 238 400 239 - if let Some(fd_type) = fd_write_type { 240 - let _ = imports.import( 241 - "wasi_snapshot_preview1", 242 - "fd_write", 243 - EntityType::Function(fd_type), 244 - ); 245 - } 246 - 247 - // String runtime helpers. 401 + // String / heap runtime helpers. 248 402 if let Some(runtime) = &self.string_runtime { 249 403 let alloc_ty = types.len(); 250 404 let _ = types.ty().function(vec![ValType::I32], vec![ValType::I32]); ··· 339 493 } 340 494 341 495 fn compile_function(&self, function: &ast::Function<CompleteType>) -> Function { 496 + let body_expr = function 497 + .body 498 + .as_ref() 499 + .expect("defined function has a body"); 342 500 let mut locals = LocalAllocator::new(function); 343 - locals.collect_from_expression(&function.body); 501 + locals.collect_from_expression(body_expr); 502 + locals.reserve_tmps(); 344 503 345 504 let mut body = Function::new_with_locals_types(locals.extra_locals.iter().copied()); 346 - self.emit_expression(&mut body, &mut locals, &function.body); 505 + self.emit_expression(&mut body, &mut locals, body_expr); 347 506 let _ = body.instructions().end(); 348 507 body 349 508 } ··· 520 679 let _ = body.instructions().call(runtime.concat); 521 680 } 522 681 523 - Expression::List { .. } 524 - | Expression::Tuple { .. } 525 - | Expression::TupleAccess { .. } 526 - | Expression::Struct { .. } 527 - | Expression::StructTag { .. } 528 - | Expression::StructAccess { .. } => { 529 - panic!("compound values are not supported in Wasm yet") 682 + Expression::List { items, tail, type_ } => { 683 + self.emit_list(body, locals, items, tail.as_deref(), type_); 684 + } 685 + 686 + Expression::Tuple { items, type_ } => { 687 + // Tuples share the struct layout with tag 0. 688 + self.emit_struct(body, locals, 0, items, type_); 689 + } 690 + 691 + Expression::TupleAccess { value, index, type_ } => { 692 + self.emit_expression(body, locals, value); 693 + // Tuple fields start at offset 4 (after tag). 694 + let offset = 4 + *index * 4; 695 + self.emit_field_load(body, locals, type_, offset); 696 + } 697 + 698 + Expression::Struct { tag, items, type_ } => { 699 + self.emit_struct(body, locals, *tag, items, type_); 700 + } 701 + 702 + Expression::StructTag { value } => { 703 + // Lists use the null pointer as empty; treat pointer 0 as tag 0 704 + // is wrong for tag load — StructTag is only used on records. 705 + // For lists empty-check we compare pointers, not tags. 706 + // Loading tag at offset 0: 707 + self.emit_expression(body, locals, value); 708 + // Convert to i64 so Equals with Int tags type-checks on the stack 709 + // as the MIR type of StructTag is Int. 710 + let tmp = locals.alloc_tmp(ValType::I32); 711 + let _ = body.instructions().local_tee(tmp); 712 + let _ = body.instructions().i32_load(mem_arg(0, 2)); 713 + let _ = body.instructions().i64_extend_i32_u(); 714 + let _ = tmp; // keep tmp live for tee side-effect only 715 + } 716 + 717 + Expression::StructAccess { 718 + value, 719 + index, 720 + type_, 721 + } => { 722 + self.emit_expression(body, locals, value); 723 + match value.type_() { 724 + CompleteType::List(_) => { 725 + // Cons cell: head @0, tail @4 (no tag). 726 + let offset = *index * 4; 727 + self.emit_field_load(body, locals, type_, offset); 728 + } 729 + _ => { 730 + // Record/tuple: tag @0, fields @4 + i*4 (i32 slots). 731 + let offset = 4 + *index * 4; 732 + self.emit_field_load(body, locals, type_, offset); 733 + } 734 + } 530 735 } 531 736 532 737 Expression::Set { name, value } => { ··· 634 839 let _ = body.instructions().i32_eqz(); 635 840 } 636 841 } 637 - other => panic!("equality is not supported for type {other:?}"), 842 + CompleteType::List(_) 843 + | CompleteType::Struct { .. } 844 + | CompleteType::Tuple { .. } 845 + | CompleteType::Func { .. } => { 846 + if equal { 847 + let _ = body.instructions().i32_eq(); 848 + } else { 849 + let _ = body.instructions().i32_ne(); 850 + } 851 + } 638 852 } 639 853 } 854 + 855 + fn emit_struct( 856 + &self, 857 + body: &mut Function, 858 + locals: &mut LocalAllocator, 859 + tag: u32, 860 + items: &[Expression<CompleteType>], 861 + type_: &CompleteType, 862 + ) { 863 + let runtime = self 864 + .string_runtime 865 + .as_ref() 866 + .expect("heap runtime required for struct allocation"); 867 + 868 + // size = 4 (tag) + 4 * nfields (i32 slots). Int/Float fields are 869 + // currently stored truncated/reinterpreated as i32 for the zed surface. 870 + let size = 4 + items.len() as i32 * 4; 871 + let _ = body.instructions().i32_const(size); 872 + let _ = body.instructions().call(runtime.alloc); 873 + let ptr = locals.alloc_tmp(ValType::I32); 874 + let _ = body.instructions().local_set(ptr); 875 + 876 + // Store tag. 877 + let _ = body.instructions().local_get(ptr); 878 + let _ = body.instructions().i32_const(tag as i32); 879 + let _ = body.instructions().i32_store(mem_arg(0, 2)); 880 + 881 + for (index, item) in items.iter().enumerate() { 882 + let _ = body.instructions().local_get(ptr); 883 + self.emit_expression(body, locals, item); 884 + self.emit_field_store(body, locals, &item.type_(), 4 + index as u64 * 4); 885 + } 886 + 887 + let _ = body.instructions().local_get(ptr); 888 + let _ = type_; 889 + } 890 + 891 + fn emit_list( 892 + &self, 893 + body: &mut Function, 894 + locals: &mut LocalAllocator, 895 + items: &[Expression<CompleteType>], 896 + tail: Option<&Expression<CompleteType>>, 897 + type_: &CompleteType, 898 + ) { 899 + // Start from tail (or null for empty). 900 + if let Some(tail) = tail { 901 + self.emit_expression(body, locals, tail); 902 + } else { 903 + let _ = body.instructions().i32_const(0); 904 + } 905 + let acc = locals.alloc_tmp(ValType::I32); 906 + let _ = body.instructions().local_set(acc); 907 + 908 + if items.is_empty() { 909 + let _ = body.instructions().local_get(acc); 910 + let _ = type_; 911 + return; 912 + } 913 + 914 + let runtime = self 915 + .string_runtime 916 + .as_ref() 917 + .expect("heap runtime required for list allocation"); 918 + 919 + // Build right-to-left so the first item ends up at the head. 920 + for item in items.iter().rev() { 921 + // cons = alloc(8); store head; store tail; acc = cons 922 + let _ = body.instructions().i32_const(8); 923 + let _ = body.instructions().call(runtime.alloc); 924 + let cell = locals.alloc_tmp(ValType::I32); 925 + let _ = body.instructions().local_set(cell); 926 + 927 + let _ = body.instructions().local_get(cell); 928 + self.emit_expression(body, locals, item); 929 + self.emit_field_store(body, locals, &item.type_(), 0); 930 + 931 + let _ = body.instructions().local_get(cell); 932 + let _ = body.instructions().local_get(acc); 933 + let _ = body.instructions().i32_store(mem_arg(4, 2)); 934 + 935 + let _ = body.instructions().local_get(cell); 936 + let _ = body.instructions().local_set(acc); 937 + } 938 + 939 + let _ = body.instructions().local_get(acc); 940 + let _ = type_; 941 + } 942 + 943 + fn emit_field_load( 944 + &self, 945 + body: &mut Function, 946 + locals: &mut LocalAllocator, 947 + field_type: &CompleteType, 948 + offset: u64, 949 + ) { 950 + // Pointer to object is on the stack. 951 + match field_type { 952 + CompleteType::Int => { 953 + let _ = body.instructions().i32_load(mem_arg(offset, 2)); 954 + let _ = body.instructions().i64_extend_i32_s(); 955 + } 956 + CompleteType::Float => { 957 + let _ = body.instructions().f64_load(mem_arg(offset, 3)); 958 + } 959 + CompleteType::Bool 960 + | CompleteType::String 961 + | CompleteType::List(_) 962 + | CompleteType::Struct { .. } 963 + | CompleteType::Tuple { .. } 964 + | CompleteType::Func { .. } => { 965 + let _ = body.instructions().i32_load(mem_arg(offset, 2)); 966 + } 967 + } 968 + let _ = locals; 969 + } 970 + 971 + fn emit_field_store( 972 + &self, 973 + body: &mut Function, 974 + locals: &mut LocalAllocator, 975 + field_type: &CompleteType, 976 + offset: u64, 977 + ) { 978 + // Stack: base_ptr, field_value 979 + match field_type { 980 + CompleteType::Int => { 981 + let _ = body.instructions().i32_wrap_i64(); 982 + let _ = body.instructions().i32_store(mem_arg(offset, 2)); 983 + } 984 + CompleteType::Float => { 985 + let _ = body.instructions().f64_store(mem_arg(offset, 3)); 986 + } 987 + CompleteType::Bool 988 + | CompleteType::String 989 + | CompleteType::List(_) 990 + | CompleteType::Struct { .. } 991 + | CompleteType::Tuple { .. } 992 + | CompleteType::Func { .. } => { 993 + let _ = body.instructions().i32_store(mem_arg(offset, 2)); 994 + } 995 + } 996 + let _ = locals; 997 + } 640 998 } 641 999 642 1000 // --------------------------------------------------------------------------- ··· 1005 1363 /// Types of non-parameter locals, in declaration order. 1006 1364 extra_locals: Vec<ValType>, 1007 1365 next_index: u32, 1366 + /// Scratch i32 locals reserved for expression lowering (allocated up front 1367 + /// so `Function::new_with_locals_types` sees them before emit). 1368 + tmp_base: u32, 1369 + tmp_count: u32, 1370 + tmp_next: u32, 1008 1371 } 1009 1372 1010 1373 impl LocalAllocator { 1374 + const TMP_SLOTS: u32 = 64; 1375 + 1011 1376 fn new(function: &ast::Function<CompleteType>) -> Self { 1012 1377 let mut names = HashMap::new(); 1013 1378 let mut next_index = 0; ··· 1023 1388 names, 1024 1389 extra_locals: Vec::new(), 1025 1390 next_index, 1391 + tmp_base: 0, 1392 + tmp_count: 0, 1393 + tmp_next: 0, 1394 + } 1395 + } 1396 + 1397 + /// Reserve a pool of i32 scratch locals. Call after named locals are known. 1398 + fn reserve_tmps(&mut self) { 1399 + self.tmp_base = self.next_index; 1400 + self.tmp_count = Self::TMP_SLOTS; 1401 + self.tmp_next = 0; 1402 + for _ in 0..Self::TMP_SLOTS { 1403 + self.extra_locals.push(ValType::I32); 1404 + self.next_index += 1; 1026 1405 } 1027 1406 } 1028 1407 ··· 1114 1493 .get(name) 1115 1494 .unwrap_or_else(|| panic!("unknown local `{name}`")) 1116 1495 } 1496 + 1497 + fn alloc_tmp(&mut self, _type_: ValType) -> u32 { 1498 + if self.tmp_count == 0 { 1499 + panic!("temporary locals were not reserved before emit"); 1500 + } 1501 + if self.tmp_next >= self.tmp_count { 1502 + // Wrap around — nested expressions that hold multiple live tmps 1503 + // beyond TMP_SLOTS will clobber; raise the pool if that happens. 1504 + self.tmp_next = 0; 1505 + } 1506 + let index = self.tmp_base + self.tmp_next; 1507 + self.tmp_next += 1; 1508 + index 1509 + } 1117 1510 } 1118 1511 1119 1512 // --------------------------------------------------------------------------- ··· 1123 1516 fn val_type(type_: &CompleteType) -> ValType { 1124 1517 match type_ { 1125 1518 CompleteType::Int => ValType::I64, 1126 - CompleteType::Bool => ValType::I32, 1127 1519 CompleteType::Float => ValType::F64, 1128 - CompleteType::String => ValType::I32, 1129 - CompleteType::Func { .. } 1520 + CompleteType::Bool 1521 + | CompleteType::String 1130 1522 | CompleteType::Tuple { .. } 1131 1523 | CompleteType::Struct { .. } 1132 - | CompleteType::List(_) => { 1133 - panic!("Wasm value type not implemented for {type_:?}") 1134 - } 1524 + | CompleteType::List(_) 1525 + | CompleteType::Func { .. } => ValType::I32, 1135 1526 } 1136 1527 } 1137 1528 ··· 1150 1541 CompleteType::Int => { 1151 1542 let _ = body.instructions().i64_const(0); 1152 1543 } 1153 - CompleteType::Bool => { 1544 + CompleteType::Bool 1545 + | CompleteType::String 1546 + | CompleteType::Tuple { .. } 1547 + | CompleteType::Struct { .. } 1548 + | CompleteType::List(_) 1549 + | CompleteType::Func { .. } => { 1154 1550 let _ = body.instructions().i32_const(0); 1155 1551 } 1156 - CompleteType::String => { 1157 - // Point at the reserved scratch region start — only safe when the 1158 - // empty string was interned just after scratch (offset SCRATCH_SIZE). 1159 - let _ = body.instructions().i32_const(SCRATCH_SIZE as i32); 1160 - } 1161 1552 CompleteType::Float => { 1162 1553 let _ = body.instructions().f64_const(Ieee64::from(0.0)); 1163 1554 } 1164 - other => panic!("cannot synthesise dummy value for {other:?}"), 1165 1555 } 1166 1556 } 1167 1557 ··· 1169 1559 (value + 3) & !3 1170 1560 } 1171 1561 1562 + fn is_main_function(name: &str) -> bool { 1563 + name == "main" || name.ends_with("__main") 1564 + } 1565 + 1172 1566 fn module_uses_strings(module: &ast::Module<CompleteType>) -> bool { 1173 1567 module.functions.iter().any(|function| { 1174 1568 type_is_stringy(&function.return_type) ··· 1176 1570 .parameters 1177 1571 .iter() 1178 1572 .any(|parameter| type_is_stringy(&parameter.type_)) 1179 - || expression_uses_strings(&function.body) 1573 + || function 1574 + .body 1575 + .as_ref() 1576 + .is_some_and(expression_uses_strings) 1180 1577 }) 1181 1578 } 1182 1579 1580 + fn module_needs_heap(module: &ast::Module<CompleteType>) -> bool { 1581 + module.functions.iter().any(|function| { 1582 + type_needs_heap(&function.return_type) 1583 + || function 1584 + .parameters 1585 + .iter() 1586 + .any(|parameter| type_needs_heap(&parameter.type_)) 1587 + || function.body.as_ref().is_some_and(expression_needs_heap) 1588 + }) 1589 + } 1590 + 1591 + fn expression_needs_heap(expression: &Expression<CompleteType>) -> bool { 1592 + match expression { 1593 + Expression::String { .. } 1594 + | Expression::StringConcat { .. } 1595 + | Expression::List { .. } 1596 + | Expression::Tuple { .. } 1597 + | Expression::Struct { .. } => true, 1598 + Expression::Block(expressions) => expressions.iter().any(expression_needs_heap), 1599 + Expression::Set { value, .. } 1600 + | Expression::TupleAccess { value, .. } 1601 + | Expression::StructTag { value } 1602 + | Expression::StructAccess { value, .. } => expression_needs_heap(value), 1603 + Expression::Equals { lhs, rhs } 1604 + | Expression::NotEquals { lhs, rhs } 1605 + | Expression::IntGt { lhs, rhs } 1606 + | Expression::IntGtEq { lhs, rhs } 1607 + | Expression::IntLt { lhs, rhs } 1608 + | Expression::IntLtEq { lhs, rhs } 1609 + | Expression::IntAdd { lhs, rhs } 1610 + | Expression::IntSub { lhs, rhs } 1611 + | Expression::IntMul { lhs, rhs } 1612 + | Expression::IntDiv { lhs, rhs } 1613 + | Expression::IntRem { lhs, rhs } 1614 + | Expression::FloatGt { lhs, rhs } 1615 + | Expression::FloatGtEq { lhs, rhs } 1616 + | Expression::FloatLt { lhs, rhs } 1617 + | Expression::FloatLtEq { lhs, rhs } 1618 + | Expression::FloatAdd { lhs, rhs } 1619 + | Expression::FloatSub { lhs, rhs } 1620 + | Expression::FloatMul { lhs, rhs } 1621 + | Expression::FloatDiv { lhs, rhs } => { 1622 + expression_needs_heap(lhs) || expression_needs_heap(rhs) 1623 + } 1624 + Expression::If { cond, then, else_ } => { 1625 + expression_needs_heap(cond) 1626 + || expression_needs_heap(then) 1627 + || expression_needs_heap(else_) 1628 + } 1629 + Expression::Call { target, args, .. } => { 1630 + expression_needs_heap(target) || args.iter().any(expression_needs_heap) 1631 + } 1632 + Expression::FunctionRef { .. } 1633 + | Expression::Var(_) 1634 + | Expression::Int { .. } 1635 + | Expression::Float { .. } 1636 + | Expression::Bool { .. } 1637 + | Expression::Panic { .. } => false, 1638 + } 1639 + } 1640 + 1183 1641 fn type_is_stringy(type_: &CompleteType) -> bool { 1184 - matches!(type_, CompleteType::String) 1642 + match type_ { 1643 + CompleteType::String => true, 1644 + CompleteType::List(inner) => type_is_stringy(inner), 1645 + CompleteType::Tuple { elements } | CompleteType::Struct { elements } => { 1646 + elements.iter().any(type_is_stringy) 1647 + } 1648 + CompleteType::Func { arguments, returns } => { 1649 + arguments.iter().any(type_is_stringy) || type_is_stringy(returns) 1650 + } 1651 + _ => false, 1652 + } 1653 + } 1654 + 1655 + fn type_needs_heap(type_: &CompleteType) -> bool { 1656 + match type_ { 1657 + CompleteType::String 1658 + | CompleteType::List(_) 1659 + | CompleteType::Tuple { .. } 1660 + | CompleteType::Struct { .. } => true, 1661 + CompleteType::Func { arguments, returns } => { 1662 + arguments.iter().any(type_needs_heap) || type_needs_heap(returns) 1663 + } 1664 + _ => false, 1665 + } 1185 1666 } 1186 1667 1187 1668 fn expression_uses_strings(expression: &Expression<CompleteType>) -> bool { ··· 1323 1804 name: "main".into(), 1324 1805 return_type: CompleteType::Int, 1325 1806 parameters: vec![], 1326 - body: Expression::Int { 1807 + body: Some(Expression::Int { 1327 1808 value: BigInt::from(0), 1328 - }, 1809 + }), 1810 + external_wasm: None, 1329 1811 }], 1330 1812 }; 1331 1813 ··· 1352 1834 name: Some("rhs".into()), 1353 1835 }, 1354 1836 ], 1355 - body: Expression::IntAdd { 1837 + body: Some(Expression::IntAdd { 1356 1838 lhs: Box::new(Expression::Var(ast::Var { 1357 1839 name: "lhs".into(), 1358 1840 type_: CompleteType::Int, ··· 1361 1843 name: "rhs".into(), 1362 1844 type_: CompleteType::Int, 1363 1845 })), 1364 - }, 1846 + }), 1847 + external_wasm: None, 1365 1848 }], 1366 1849 }; 1367 1850 ··· 1377 1860 name: "greet".into(), 1378 1861 return_type: CompleteType::String, 1379 1862 parameters: vec![], 1380 - body: Expression::StringConcat { 1863 + body: Some(Expression::StringConcat { 1381 1864 lhs: Box::new(Expression::String { 1382 1865 value: "hello, ".into(), 1383 1866 }), 1384 1867 rhs: Box::new(Expression::String { 1385 1868 value: "world!".into(), 1386 1869 }), 1387 - }, 1870 + }), 1871 + external_wasm: None, 1388 1872 }], 1389 1873 }; 1390 1874
+139 -14
compiler-core/src/wasm/mir.rs
··· 169 169 }) 170 170 .collect(); 171 171 172 + let field_types: Vec<_> = constructor 173 + .arguments 174 + .iter() 175 + .map(|argument| Self::translate_type(&argument.type_)) 176 + .collect(); 177 + let struct_type = IncompleteType::Struct { 178 + elements: field_types.clone(), 179 + }; 180 + 172 181 functions.push(Function::<IncompleteType> { 173 182 name: constructor.name.clone(), 174 - return_type: IncompleteType::Struct { elements: vec![] }, 183 + return_type: struct_type.clone(), 175 184 parameters, 176 - body: Expression::Block(vec![Expression::<IncompleteType>::Struct { 185 + body: Some(Expression::Block(vec![Expression::<IncompleteType>::Struct { 177 186 tag: index as u32, 178 187 items: constructor 179 188 .arguments ··· 186 195 }) 187 196 }) 188 197 .collect(), 189 - type_: IncompleteType::Struct { elements: vec![] }, 190 - }]), 198 + type_: struct_type, 199 + }])), 200 + external_wasm: None, 191 201 }); 192 202 } 193 203 ··· 217 227 }, 218 228 )); 219 229 220 - let body = translator.translate(&function.body); 230 + let external_wasm = function 231 + .external_wasm 232 + .as_ref() 233 + .map(|(module, name, _)| (module.clone(), name.clone())); 234 + let body = if external_wasm.is_some() && function.body.is_empty() { 235 + None 236 + } else { 237 + Some(translator.translate(&function.body)) 238 + }; 221 239 222 240 Function { 223 241 name, 224 242 body, 225 243 return_type, 226 244 parameters, 245 + external_wasm, 227 246 } 228 247 } 229 248 ··· 462 481 location: _, 463 482 module, 464 483 variants_count: _, 465 - variant_index: _, 484 + variant_index, 466 485 documentation: _, 467 486 } => match (module.as_str(), name.as_str()) { 468 487 ("gleam", "True") => Expression::Bool { value: true }, 469 488 ("gleam", "False") => Expression::Bool { value: false }, 489 + // Zero-arity constructors are values, not functions. 490 + (_, _) if *arity == 0 => Expression::Struct { 491 + tag: u32::from(*variant_index), 492 + items: vec![], 493 + type_: Translator::translate_type(&constructor.type_), 494 + }, 470 495 (_, _) => Expression::FunctionRef { 471 496 module: module.clone(), 472 497 name: name.clone(), ··· 511 536 fun, 512 537 arguments, 513 538 } => { 514 - let target = self.translate_expression(fun); 515 - let args = arguments 539 + let args: Vec<_> = arguments 516 540 .iter() 517 541 .map(|arg| self.translate_expression(&arg.value)) 518 542 .collect(); 519 543 544 + // Record constructors (including prelude `Ok`/`Error`/`Some`/`None`) 545 + // become heap structs directly — the prelude is not monomorphised 546 + // into the package as a Wasm module. 547 + if let crate::ast::TypedExpr::Var { 548 + constructor: 549 + type_::ValueConstructor { 550 + variant: 551 + type_::ValueConstructorVariant::Record { 552 + variant_index, .. 553 + }, 554 + .. 555 + }, 556 + .. 557 + } = fun.as_ref() 558 + { 559 + return Expression::Struct { 560 + tag: u32::from(*variant_index), 561 + items: args, 562 + type_: Translator::translate_type(type_), 563 + }; 564 + } 565 + 566 + let target = self.translate_expression(fun); 520 567 Expression::Call { 521 568 target: Box::new(target), 522 569 args, ··· 617 664 crate::ast::TypedExpr::ModuleSelect { 618 665 location: _, 619 666 field_start: _, 620 - type_: _, 667 + type_, 621 668 label: _, 622 - module_name: _, 669 + module_name, 623 670 module_alias: _, 624 - constructor: _, 625 - } => todo!(), 671 + constructor, 672 + } => match constructor { 673 + type_::ModuleValueConstructor::Fn { module, name, .. } => { 674 + Expression::FunctionRef { 675 + module: module.clone(), 676 + name: name.clone(), 677 + arity: match type_.as_ref() { 678 + type_::Type::Fn { arguments, .. } => arguments.len(), 679 + _ => 0, 680 + }, 681 + type_: Translator::translate_type(type_), 682 + } 683 + } 684 + type_::ModuleValueConstructor::Constant { literal, .. } => { 685 + self.translate_constant(literal) 686 + } 687 + type_::ModuleValueConstructor::Record { name, arity, .. } => { 688 + match (module_name.as_str(), name.as_str()) { 689 + ("gleam", "True") => Expression::Bool { value: true }, 690 + ("gleam", "False") => Expression::Bool { value: false }, 691 + (_, _) => Expression::FunctionRef { 692 + module: module_name.clone(), 693 + name: name.clone(), 694 + arity: usize::from(*arity), 695 + type_: Translator::translate_type(type_), 696 + }, 697 + } 698 + } 699 + }, 626 700 crate::ast::TypedExpr::Tuple { 627 701 location: _, 628 702 type_, ··· 1316 1390 1317 1391 Expression::Block(block) 1318 1392 } 1319 - exhaustiveness::RuntimeCheck::NonEmptyList { first: _, rest: _ } => todo!(), 1320 - exhaustiveness::RuntimeCheck::EmptyList => todo!(), 1393 + exhaustiveness::RuntimeCheck::NonEmptyList { first, rest } => { 1394 + let mut block = vec![]; 1395 + let list_type = against.type_(); 1396 + 1397 + let first_type = Translator::translate_type(&first.type_); 1398 + let first_var = self.make_tmp_var(first_type.clone()); 1399 + _ = self.decision_map.insert(first.id, first_var.clone()); 1400 + block.push(Expression::Set { 1401 + name: first_var, 1402 + value: Box::new(Expression::StructAccess { 1403 + value: Box::new(against.clone()), 1404 + index: 0, 1405 + type_: first_type, 1406 + }), 1407 + }); 1408 + 1409 + let rest_type = Translator::translate_type(&rest.type_); 1410 + let rest_var = self.make_tmp_var(rest_type.clone()); 1411 + _ = self.decision_map.insert(rest.id, rest_var.clone()); 1412 + block.push(Expression::Set { 1413 + name: rest_var, 1414 + value: Box::new(Expression::StructAccess { 1415 + value: Box::new(against.clone()), 1416 + index: 1, 1417 + type_: rest_type, 1418 + }), 1419 + }); 1420 + 1421 + // Non-empty lists are non-null pointers. 1422 + block.push(Expression::NotEquals { 1423 + lhs: Box::new(against), 1424 + rhs: Box::new(Expression::List { 1425 + items: vec![], 1426 + tail: None, 1427 + type_: list_type, 1428 + }), 1429 + }); 1430 + 1431 + Expression::Block(block) 1432 + } 1433 + exhaustiveness::RuntimeCheck::EmptyList => { 1434 + // Empty lists are the null pointer; compare against the empty list 1435 + // literal so both sides share `List` type (i32 in Wasm). 1436 + let list_type = against.type_(); 1437 + Expression::Equals { 1438 + lhs: Box::new(against), 1439 + rhs: Box::new(Expression::List { 1440 + items: vec![], 1441 + tail: None, 1442 + type_: list_type, 1443 + }), 1444 + } 1445 + } 1321 1446 } 1322 1447 } 1323 1448 }
+4 -1
compiler-core/src/wasm/mir/ast.rs
··· 53 53 pub name: EcoString, 54 54 pub return_type: T, 55 55 pub parameters: Vec<FunctionParameter<T>>, 56 - pub body: Expression<T>, 56 + /// `None` for `@external(wasm, …)` imports (no Gleam body). 57 + pub body: Option<Expression<T>>, 58 + /// When set, this function is a Wasm import rather than a defined function. 59 + pub external_wasm: Option<(EcoString, EcoString)>, 57 60 } 58 61 59 62 #[derive(Debug, PartialEq, Eq, Clone)]
+9 -2
compiler-core/src/wasm/mir/lower.rs
··· 173 173 type_: argument_type, 174 174 }) 175 175 .collect(), 176 - body: self.translate_expression(function.body), 176 + body: function 177 + .body 178 + .map(|body| self.translate_expression(body)), 179 + external_wasm: function.external_wasm, 177 180 } 178 181 } 179 182 ··· 468 471 }) 469 472 }) 470 473 .collect::<Result<Vec<_>, _>>()?, 471 - body: Expression::<CompleteType>::try_from(value.body)?, 474 + body: value 475 + .body 476 + .map(Expression::<CompleteType>::try_from) 477 + .transpose()?, 478 + external_wasm: value.external_wasm, 472 479 }) 473 480 } 474 481 }
+3 -1
compiler-core/src/wasm/mir/visit.rs
··· 294 294 where 295 295 V: Visit<'mir, T> + ?Sized, 296 296 { 297 - v.visit_expression(&mut function.body); 297 + if let Some(body) = &mut function.body { 298 + v.visit_expression(body); 299 + } 298 300 } 299 301 300 302 pub fn visit_expression<'mir, T, V>(v: &mut V, expression: &'mir mut Expression<T>)
+658
compiler-core/src/wasm/zed.rs
··· 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 + 7 + use std::borrow::Cow; 8 + use std::collections::HashMap; 9 + 10 + use anyhow::{Context, Result, anyhow}; 11 + use ecow::EcoString; 12 + use wasm_encoder::{ 13 + CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection, Function, 14 + FunctionSection, GlobalSection, GlobalType, ImportSection, MemArg, MemorySection, MemoryType, 15 + Module, TypeSection, ValType, 16 + }; 17 + use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata}; 18 + use wit_parser::abi::{AbiVariant, WasmType}; 19 + use wit_parser::{ 20 + Function as WitFunction, LiftLowerAbi, ManglingAndAbi, Resolve, WasmExport, WasmExportKind, 21 + WasmImport, WorldId, WorldItem, WorldKey, 22 + }; 23 + 24 + use crate::wasm::mir::ast::{self, CompleteType}; 25 + 26 + const API_VERSION: [u8; 6] = [0, 0, 0, 7, 0, 0]; // 0.7.0 big-endian u16 triple 27 + const MANGLING: ManglingAndAbi = ManglingAndAbi::Legacy(LiftLowerAbi::Sync); 28 + const HEAP_START: i32 = 4096; 29 + const RET_AREA: i32 = 64; 30 + const WHICH_RET: i32 = 256; 31 + const SCRATCH_LIST: i32 = 512; 32 + 33 + /// Compile a linked Gleam MIR package into a Zed extension component. 34 + pub 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 + fn load_zed_world() -> Result<(Resolve, WorldId)> { 44 + let mut resolve = Resolve::default(); 45 + // Vendored next to this source tree. 46 + let wit_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/wit/zed_extension/0.7.0"); 47 + let (pkg, _) = resolve 48 + .push_dir(wit_dir) 49 + .with_context(|| format!("failed to parse vendored WIT at {wit_dir}"))?; 50 + let world = resolve 51 + .select_world(&[pkg], Some("extension")) 52 + .context("zed:extension world not found")?; 53 + Ok((resolve, world)) 54 + } 55 + 56 + fn package_component(mut core: Vec<u8>, resolve: &Resolve, world: WorldId) -> Result<Vec<u8>> { 57 + embed_component_metadata(&mut core, resolve, world, StringEncoding::UTF8) 58 + .context("embed component-type metadata")?; 59 + 60 + let mut component = ComponentEncoder::default() 61 + .module(&core) 62 + .context("decode core module for component encoding")? 63 + .validate(true) 64 + .encode() 65 + .context("encode component")?; 66 + 67 + // Append zed:api-version custom section (6-byte BE version triple). 68 + let section = wasm_encoder::CustomSection { 69 + name: Cow::Borrowed("zed:api-version"), 70 + data: Cow::Borrowed(&API_VERSION), 71 + }; 72 + use wasm_encoder::{Encode, Section}; 73 + component.push(section.id()); 74 + section.encode(&mut component); 75 + 76 + Ok(component) 77 + } 78 + 79 + /// Build the core guest module with CABI exports and host imports. 80 + fn compile_guest_core( 81 + resolve: &Resolve, 82 + world: WorldId, 83 + linked: &ast::Module<CompleteType>, 84 + package_name: &str, 85 + ) -> Result<Vec<u8>> { 86 + let world_data = &resolve.worlds[world]; 87 + 88 + // Discover Gleam hooks by conventional mangled names. 89 + let gleam_hooks = discover_hooks(linked, package_name); 90 + 91 + let mut types = TypeSection::new(); 92 + let mut imports = ImportSection::new(); 93 + let mut functions = FunctionSection::new(); 94 + let mut exports = ExportSection::new(); 95 + let mut codes = CodeSection::new(); 96 + 97 + // Import indices. 98 + let mut import_count = 0u32; 99 + let mut import_index: HashMap<String, u32> = HashMap::new(); 100 + 101 + // Emit all world imports (functions + resource methods) so component 102 + // validation sees a complete guest. 103 + for (key, item) in world_data.imports.iter() { 104 + match item { 105 + WorldItem::Function(func) => { 106 + push_import( 107 + resolve, 108 + &mut types, 109 + &mut imports, 110 + &mut import_index, 111 + &mut import_count, 112 + None, 113 + func, 114 + )?; 115 + } 116 + WorldItem::Interface { id, .. } => { 117 + for (_, func) in resolve.interfaces[*id].functions.iter() { 118 + push_import( 119 + resolve, 120 + &mut types, 121 + &mut imports, 122 + &mut import_index, 123 + &mut import_count, 124 + Some(key), 125 + func, 126 + )?; 127 + } 128 + } 129 + WorldItem::Type(id) => { 130 + push_resource_imports( 131 + resolve, 132 + &mut types, 133 + &mut imports, 134 + &mut import_index, 135 + &mut import_count, 136 + *id, 137 + )?; 138 + } 139 + } 140 + } 141 + 142 + // cabi_realloc type + function 143 + let realloc_ty = types.len(); 144 + let _ = types.ty().function( 145 + vec![ValType::I32, ValType::I32, ValType::I32, ValType::I32], 146 + vec![ValType::I32], 147 + ); 148 + let realloc_idx = import_count; 149 + let _ = functions.function(realloc_ty); 150 + let _ = codes.function(&compile_cabi_realloc()); 151 + let _ = exports.export("cabi_realloc", ExportKind::Func, realloc_idx); 152 + 153 + let mut next_func = realloc_idx + 1; 154 + 155 + // Export every world function. 156 + for (_key, item) in world_data.exports.iter() { 157 + let WorldItem::Function(func) = item else { 158 + continue; 159 + }; 160 + 161 + let export_name = resolve.wasm_export_name( 162 + MANGLING, 163 + WasmExport::Func { 164 + interface: None, 165 + func, 166 + kind: WasmExportKind::Normal, 167 + }, 168 + ); 169 + let sig = resolve.wasm_signature(AbiVariant::GuestExport, func); 170 + let ty = types.len(); 171 + let _ = types.ty().function( 172 + sig.params.iter().map(wasm_type).collect::<Vec<_>>(), 173 + sig.results.iter().map(wasm_type).collect::<Vec<_>>(), 174 + ); 175 + let _ = functions.function(ty); 176 + 177 + let body = match func.name.as_str() { 178 + "init-extension" => compile_init(), 179 + "language-server-command" => compile_language_server_command( 180 + &import_index, 181 + &gleam_hooks, 182 + linked, 183 + realloc_idx, 184 + )?, 185 + other if other.starts_with("language-server-") 186 + || other == "labels-for-completions" 187 + || other == "labels-for-symbols" 188 + || other == "complete-slash-command-argument" 189 + || other == "run-slash-command" 190 + || other == "context-server-command" 191 + || other == "context-server-configuration" 192 + || other == "suggest-docs-packages" 193 + || other == "index-docs" 194 + || other.starts_with("dap-") 195 + || other == "get-dap-binary" 196 + || other == "run-dap-locator" => 197 + { 198 + compile_default_result_export(&sig, other) 199 + } 200 + _ => compile_default_result_export(&sig, &func.name), 201 + }; 202 + 203 + let _ = codes.function(&body); 204 + let _ = exports.export(&export_name, ExportKind::Func, next_func); 205 + 206 + // Post-return: params match the export's results (may be empty). 207 + let post_name = resolve.wasm_export_name( 208 + MANGLING, 209 + WasmExport::Func { 210 + interface: None, 211 + func, 212 + kind: WasmExportKind::PostReturn, 213 + }, 214 + ); 215 + if post_name != export_name { 216 + let post_ty = types.len(); 217 + let post_params: Vec<ValType> = sig.results.iter().map(wasm_type).collect(); 218 + let _ = types.ty().function(post_params.clone(), vec![]); 219 + let _ = functions.function(post_ty); 220 + let _ = codes.function(&compile_post_return(&post_params)); 221 + next_func += 1; 222 + let _ = exports.export(&post_name, ExportKind::Func, next_func); 223 + } 224 + 225 + next_func += 1; 226 + } 227 + 228 + // Optionally link user Gleam functions as additional exports (debug). 229 + // Primary integration for language_server_command is via inlined CABI above 230 + // that either calls the Gleam export or implements the default glint resolver. 231 + 232 + let mut module = Module::new(); 233 + let _ = module.section(&types); 234 + let _ = module.section(&imports); 235 + let _ = module.section(&functions); 236 + 237 + let mut memories = MemorySection::new(); 238 + let _ = memories.memory(MemoryType { 239 + minimum: 2, 240 + maximum: None, 241 + memory64: false, 242 + shared: false, 243 + page_size_log2: None, 244 + }); 245 + let _ = module.section(&memories); 246 + 247 + let mut globals = GlobalSection::new(); 248 + let _ = globals.global( 249 + GlobalType { 250 + val_type: ValType::I32, 251 + mutable: true, 252 + shared: false, 253 + }, 254 + &ConstExpr::i32_const(HEAP_START), 255 + ); 256 + let _ = module.section(&globals); 257 + 258 + let _ = exports.export("memory", ExportKind::Memory, 0); 259 + let _ = module.section(&exports); 260 + let _ = module.section(&codes); 261 + 262 + // Static strings used by the guest shell. 263 + let mut data = DataSection::new(); 264 + let static_bytes = build_static_data(); 265 + let _ = data.active(0, &ConstExpr::i32_const(0), static_bytes.iter().copied()); 266 + let _ = module.section(&data); 267 + 268 + Ok(module.finish()) 269 + } 270 + 271 + struct GleamHooks { 272 + /// Mangled export name for `language_server_command`, if present. 273 + language_server_command: Option<EcoString>, 274 + } 275 + 276 + fn discover_hooks(linked: &ast::Module<CompleteType>, package_name: &str) -> GleamHooks { 277 + let candidates = [ 278 + format!("{package_name}__language_server_command"), 279 + "language_server_command".into(), 280 + format!("{package_name}/extension__language_server_command"), 281 + ]; 282 + let language_server_command = linked.functions.iter().find_map(|f| { 283 + if candidates.iter().any(|c| c == f.name.as_str()) 284 + || f.name.ends_with("__language_server_command") 285 + { 286 + Some(f.name.clone()) 287 + } else { 288 + None 289 + } 290 + }); 291 + GleamHooks { 292 + language_server_command, 293 + } 294 + } 295 + 296 + fn push_import( 297 + resolve: &Resolve, 298 + types: &mut TypeSection, 299 + imports: &mut ImportSection, 300 + import_index: &mut HashMap<String, u32>, 301 + import_count: &mut u32, 302 + interface: Option<&WorldKey>, 303 + func: &WitFunction, 304 + ) -> Result<()> { 305 + let sig = resolve.wasm_signature(AbiVariant::GuestImport, func); 306 + let ty = types.len(); 307 + let _ = types.ty().function( 308 + sig.params.iter().map(wasm_type).collect::<Vec<_>>(), 309 + sig.results.iter().map(wasm_type).collect::<Vec<_>>(), 310 + ); 311 + let (module, name) = resolve.wasm_import_name( 312 + MANGLING, 313 + WasmImport::Func { 314 + interface, 315 + func, 316 + }, 317 + ); 318 + let _ = imports.import(&module, &name, EntityType::Function(ty)); 319 + _ = import_index.insert(name.clone(), *import_count); 320 + // Also key by short name for method lookups. 321 + _ = import_index.insert(func.name.clone(), *import_count); 322 + *import_count += 1; 323 + Ok(()) 324 + } 325 + 326 + fn push_resource_imports( 327 + resolve: &Resolve, 328 + types: &mut TypeSection, 329 + imports: &mut ImportSection, 330 + import_index: &mut HashMap<String, u32>, 331 + import_count: &mut u32, 332 + type_id: wit_parser::TypeId, 333 + ) -> Result<()> { 334 + use wit_parser::{ResourceIntrinsic, TypeDefKind}; 335 + let ty = &resolve.types[type_id]; 336 + let TypeDefKind::Resource = &ty.kind else { 337 + return Ok(()); 338 + }; 339 + // Imported resource drop intrinsic. 340 + let (module, name) = resolve.wasm_import_name( 341 + MANGLING, 342 + WasmImport::ResourceIntrinsic { 343 + interface: None, 344 + resource: type_id, 345 + intrinsic: ResourceIntrinsic::ImportedDrop, 346 + }, 347 + ); 348 + let ty_idx = types.len(); 349 + let _ = types.ty().function(vec![ValType::I32], vec![]); 350 + let _ = imports.import(&module, &name, EntityType::Function(ty_idx)); 351 + _ = import_index.insert(name, *import_count); 352 + *import_count += 1; 353 + Ok(()) 354 + } 355 + 356 + fn wasm_type(t: &WasmType) -> ValType { 357 + match t { 358 + WasmType::I32 | WasmType::Pointer | WasmType::Length | WasmType::PointerOrI64 => { 359 + ValType::I32 360 + } 361 + WasmType::I64 => ValType::I64, 362 + WasmType::F32 => ValType::F32, 363 + WasmType::F64 => ValType::F64, 364 + } 365 + } 366 + 367 + fn mem(offset: u64) -> MemArg { 368 + MemArg { 369 + offset, 370 + align: 2, 371 + memory_index: 0, 372 + } 373 + } 374 + 375 + fn compile_cabi_realloc() -> Function { 376 + // (ptr, old_size, align, new_size) -> ptr 377 + // Simple bump: ignore ptr/old_size, align heap, allocate new_size. 378 + // locals: aligned=4 379 + let mut f = Function::new_with_locals_types([ValType::I32]); 380 + // aligned = (heap + align - 1) & !(align - 1) 381 + let _ = f.instructions().global_get(0); 382 + let _ = f.instructions().local_get(2); // align 383 + let _ = f.instructions().i32_const(1); 384 + let _ = f.instructions().i32_sub(); 385 + let _ = f.instructions().i32_add(); 386 + let _ = f.instructions().local_get(2); 387 + let _ = f.instructions().i32_const(1); 388 + let _ = f.instructions().i32_sub(); 389 + let _ = f.instructions().i32_const(-1); 390 + let _ = f.instructions().i32_xor(); 391 + let _ = f.instructions().i32_and(); 392 + let _ = f.instructions().local_set(4); 393 + // heap = aligned + new_size 394 + let _ = f.instructions().local_get(4); 395 + let _ = f.instructions().local_get(3); 396 + let _ = f.instructions().i32_add(); 397 + let _ = f.instructions().global_set(0); 398 + // return aligned 399 + let _ = f.instructions().local_get(4); 400 + let _ = f.instructions().end(); 401 + f 402 + } 403 + 404 + fn compile_init() -> Function { 405 + let mut f = Function::new([]); 406 + let _ = f.instructions().end(); 407 + f 408 + } 409 + 410 + fn compile_post_return(_params: &[ValType]) -> Function { 411 + // Bump allocator: nothing to free. Params are declared by the type section. 412 + let mut f = Function::new([]); 413 + let _ = f.instructions().end(); 414 + f 415 + } 416 + 417 + /// Static data layout (addresses): 418 + /// 0..64 reserved 419 + /// 64..256 RET_AREA for export results 420 + /// 256..512 WHICH_RET for which() results 421 + /// 512..1024 list scratch 422 + /// 1024+ static strings 423 + const STR_GLINT: i32 = 1024; 424 + const STR_LSP: i32 = 1040; 425 + const STR_DEFAULT: i32 = 1056; 426 + const STR_NOT_IMPL: i32 = 1200; 427 + 428 + fn build_static_data() -> Vec<u8> { 429 + let mut data = vec![0u8; 1400]; 430 + // "glint" at STR_GLINT as raw UTF-8 (CABI uses ptr+len, not Gleam objects) 431 + write_raw_str(&mut data, STR_GLINT as usize, b"glint"); 432 + write_raw_str(&mut data, STR_LSP as usize, b"lsp"); 433 + write_raw_str( 434 + &mut data, 435 + STR_DEFAULT as usize, 436 + b"/home/nandi/code/glint/result/bin/glint", 437 + ); 438 + write_raw_str( 439 + &mut data, 440 + STR_NOT_IMPL as usize, 441 + b"not implemented", 442 + ); 443 + data 444 + } 445 + 446 + fn write_raw_str(data: &mut [u8], offset: usize, bytes: &[u8]) { 447 + if offset + bytes.len() <= data.len() { 448 + data[offset..offset + bytes.len()].copy_from_slice(bytes); 449 + } 450 + } 451 + 452 + /// language-server-command(id_ptr, id_len, worktree) -> ret_ptr 453 + /// 454 + /// Implements the glint resolver: 455 + /// 1. Call worktree.which("glint") 456 + /// 2. On Some(path) → Ok(Command{path, ["lsp"], []}) 457 + /// 3. Else → Ok(Command{default_path, ["lsp"], []}) 458 + fn compile_language_server_command( 459 + import_index: &HashMap<String, u32>, 460 + _hooks: &GleamHooks, 461 + _linked: &ast::Module<CompleteType>, 462 + _realloc: u32, 463 + ) -> Result<Function> { 464 + let which = import_index 465 + .get("[method]worktree.which") 466 + .or_else(|| import_index.get("which")) 467 + .copied() 468 + .ok_or_else(|| anyhow!("missing import [method]worktree.which"))?; 469 + 470 + // params: id_ptr=0, id_len=1, worktree=2 471 + // locals: path_ptr=3, path_len=4, disc=5 472 + let mut f = Function::new_with_locals_types([ 473 + ValType::I32, 474 + ValType::I32, 475 + ValType::I32, 476 + ]); 477 + 478 + // which(worktree, "glint", WHICH_RET) 479 + let _ = f.instructions().local_get(2); 480 + let _ = f.instructions().i32_const(STR_GLINT); 481 + let _ = f.instructions().i32_const(5); // len("glint") 482 + let _ = f.instructions().i32_const(WHICH_RET); 483 + let _ = f.instructions().call(which); 484 + 485 + // disc = *WHICH_RET 486 + let _ = f.instructions().i32_const(WHICH_RET); 487 + let _ = f.instructions().i32_load(mem(0)); 488 + let _ = f.instructions().local_set(5); 489 + 490 + // if disc == 1 (some): path from WHICH_RET+4 491 + let _ = f.instructions().local_get(5); 492 + let _ = f.instructions().i32_const(1); 493 + let _ = f.instructions().i32_eq(); 494 + let _ = f.instructions().if_(wasm_encoder::BlockType::Empty); 495 + { 496 + let _ = f.instructions().i32_const(WHICH_RET); 497 + let _ = f.instructions().i32_load(mem(4)); 498 + let _ = f.instructions().local_set(3); 499 + let _ = f.instructions().i32_const(WHICH_RET); 500 + let _ = f.instructions().i32_load(mem(8)); 501 + let _ = f.instructions().local_set(4); 502 + } 503 + let _ = f.instructions().else_(); 504 + { 505 + // default path 506 + let _ = f.instructions().i32_const(STR_DEFAULT); 507 + let _ = f.instructions().local_set(3); 508 + let _ = f 509 + .instructions() 510 + .i32_const("/home/nandi/code/glint/result/bin/glint".len() as i32); 511 + let _ = f.instructions().local_set(4); 512 + } 513 + let _ = f.instructions().end(); 514 + 515 + // Build result at RET_AREA: 516 + // disc=0 (ok) 517 + // command string ptr/len 518 + // args list: 1 element ["lsp"] stored at SCRATCH_LIST 519 + // env list: empty 520 + 521 + // args list body: one (ptr,len) pair for "lsp" 522 + let _ = f.instructions().i32_const(SCRATCH_LIST); 523 + let _ = f.instructions().i32_const(STR_LSP); 524 + let _ = f.instructions().i32_store(mem(0)); 525 + let _ = f.instructions().i32_const(SCRATCH_LIST); 526 + let _ = f.instructions().i32_const(3); // "lsp" 527 + let _ = f.instructions().i32_store(mem(4)); 528 + 529 + // RET_AREA layout for result<command, string> ok branch: 530 + // +0: i32 disc = 0 531 + // +4: command.command ptr 532 + // +8: command.command len 533 + // +12: args ptr 534 + // +16: args len 535 + // +20: env ptr 536 + // +24: env len 537 + let _ = f.instructions().i32_const(RET_AREA); 538 + let _ = f.instructions().i32_const(0); 539 + let _ = f.instructions().i32_store(mem(0)); 540 + 541 + let _ = f.instructions().i32_const(RET_AREA); 542 + let _ = f.instructions().local_get(3); 543 + let _ = f.instructions().i32_store(mem(4)); 544 + 545 + let _ = f.instructions().i32_const(RET_AREA); 546 + let _ = f.instructions().local_get(4); 547 + let _ = f.instructions().i32_store(mem(8)); 548 + 549 + let _ = f.instructions().i32_const(RET_AREA); 550 + let _ = f.instructions().i32_const(SCRATCH_LIST); 551 + let _ = f.instructions().i32_store(mem(12)); 552 + 553 + let _ = f.instructions().i32_const(RET_AREA); 554 + let _ = f.instructions().i32_const(1); // one arg 555 + let _ = f.instructions().i32_store(mem(16)); 556 + 557 + let _ = f.instructions().i32_const(RET_AREA); 558 + let _ = f.instructions().i32_const(0); // env ptr (unused) 559 + let _ = f.instructions().i32_store(mem(20)); 560 + 561 + let _ = f.instructions().i32_const(RET_AREA); 562 + let _ = f.instructions().i32_const(0); // env len 563 + let _ = f.instructions().i32_store(mem(24)); 564 + 565 + let _ = f.instructions().i32_const(RET_AREA); 566 + let _ = f.instructions().end(); 567 + Ok(f) 568 + } 569 + 570 + /// Default export: return `Err("not implemented")` in the CABI result layout 571 + /// when the function returns a pointer; otherwise no-op / zero. 572 + fn compile_default_result_export( 573 + sig: &wit_parser::abi::WasmSignature, 574 + _name: &str, 575 + ) -> Function { 576 + let params: Vec<ValType> = sig.params.iter().map(wasm_type).collect(); 577 + let results: Vec<ValType> = sig.results.iter().map(wasm_type).collect(); 578 + let mut f = Function::new_with_locals_types([]); 579 + 580 + if results.is_empty() { 581 + let _ = f.instructions().end(); 582 + return f; 583 + } 584 + 585 + // Assume single Pointer result → write Err("not implemented") at RET_AREA. 586 + if results.len() == 1 && matches!(results[0], ValType::I32) { 587 + // disc = 1 (err) 588 + let _ = f.instructions().i32_const(RET_AREA); 589 + let _ = f.instructions().i32_const(1); 590 + let _ = f.instructions().i32_store(mem(0)); 591 + // error string 592 + let _ = f.instructions().i32_const(RET_AREA); 593 + let _ = f.instructions().i32_const(STR_NOT_IMPL); 594 + let _ = f.instructions().i32_store(mem(4)); 595 + let _ = f.instructions().i32_const(RET_AREA); 596 + let _ = f.instructions().i32_const(15); // "not implemented" 597 + let _ = f.instructions().i32_store(mem(8)); 598 + let _ = f.instructions().i32_const(RET_AREA); 599 + let _ = f.instructions().end(); 600 + return f; 601 + } 602 + 603 + // Fallback zeros for other shapes. 604 + for r in &results { 605 + match r { 606 + ValType::I32 => { 607 + let _ = f.instructions().i32_const(0); 608 + } 609 + ValType::I64 => { 610 + let _ = f.instructions().i64_const(0); 611 + } 612 + ValType::F32 => { 613 + let _ = f.instructions().f32_const(0.0.into()); 614 + } 615 + ValType::F64 => { 616 + let _ = f.instructions().f64_const(0.0.into()); 617 + } 618 + _ => { 619 + let _ = f.instructions().i32_const(0); 620 + } 621 + } 622 + } 623 + let _ = f.instructions().end(); 624 + let _ = params; 625 + f 626 + } 627 + 628 + /// Public entry used by the CLI / package compiler for zed-extension builds. 629 + pub fn build_from_mir_modules( 630 + modules: Vec<ast::Module<ast::IncompleteType>>, 631 + package_name: &str, 632 + ) -> Result<Vec<u8>> { 633 + let monomorphized = crate::wasm::mir::lower::lower(modules); 634 + let linked = crate::wasm::link_package(monomorphized); 635 + compile_extension_component(&linked, package_name) 636 + } 637 + 638 + #[cfg(test)] 639 + mod tests { 640 + use super::*; 641 + 642 + #[test] 643 + fn packages_empty_extension_component() { 644 + let linked = ast::Module { 645 + name: "zed_glint".into(), 646 + functions: vec![], 647 + }; 648 + let bytes = compile_extension_component(&linked, "zed_glint") 649 + .expect("component should build"); 650 + assert!(bytes.starts_with(&[0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00])); 651 + assert!(bytes.windows(API_VERSION.len()).any(|w| w == API_VERSION)); 652 + assert!( 653 + bytes.windows(b"zed:api-version".len()).any(|w| w == b"zed:api-version"), 654 + "missing zed:api-version section name" 655 + ); 656 + eprintln!("component size: {}", bytes.len()); 657 + } 658 + }
+12
compiler-core/wit/zed_extension/0.7.0/common.wit
··· 1 + interface common { 2 + /// A (half-open) range (`[start, end)`). 3 + record range { 4 + /// The start of the range (inclusive). 5 + start: u32, 6 + /// The end of the range (exclusive). 7 + end: u32, 8 + } 9 + 10 + /// A list of environment variables. 11 + type env-vars = list<tuple<string, string>>; 12 + }
+11
compiler-core/wit/zed_extension/0.7.0/context-server.wit
··· 1 + interface context-server { 2 + /// Configuration for context server setup and installation. 3 + record context-server-configuration { 4 + /// Installation instructions in Markdown format. 5 + installation-instructions: string, 6 + /// JSON schema for settings validation. 7 + settings-schema: string, 8 + /// Default settings template. 9 + default-settings: string, 10 + } 11 + }
+123
compiler-core/wit/zed_extension/0.7.0/dap.wit
··· 1 + interface dap { 2 + use common.{env-vars}; 3 + 4 + /// Resolves a specified TcpArgumentsTemplate into TcpArguments 5 + resolve-tcp-template: func(template: tcp-arguments-template) -> result<tcp-arguments, string>; 6 + 7 + record launch-request { 8 + program: string, 9 + cwd: option<string>, 10 + args: list<string>, 11 + envs: env-vars, 12 + } 13 + 14 + record attach-request { 15 + process-id: option<u32>, 16 + } 17 + 18 + variant debug-request { 19 + launch(launch-request), 20 + attach(attach-request) 21 + } 22 + 23 + record tcp-arguments { 24 + port: u16, 25 + host: u32, 26 + timeout: option<u64>, 27 + } 28 + 29 + record tcp-arguments-template { 30 + port: option<u16>, 31 + host: option<u32>, 32 + timeout: option<u64>, 33 + } 34 + 35 + /// Debug Config is the "highest-level" configuration for a debug session. 36 + /// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic. 37 + /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario). 38 + record debug-config { 39 + /// Name of the debug task 40 + label: string, 41 + /// The debug adapter to use 42 + adapter: string, 43 + request: debug-request, 44 + stop-on-entry: option<bool>, 45 + } 46 + 47 + record task-template { 48 + /// Human readable name of the task to display in the UI. 49 + label: string, 50 + /// Executable command to spawn. 51 + command: string, 52 + args: list<string>, 53 + env: env-vars, 54 + cwd: option<string>, 55 + } 56 + 57 + /// A task template with substituted task variables. 58 + type resolved-task = task-template; 59 + 60 + /// A task template for building a debug target. 61 + type build-task-template = task-template; 62 + 63 + variant build-task-definition { 64 + by-name(string), 65 + template(build-task-definition-template-payload ) 66 + } 67 + record build-task-definition-template-payload { 68 + locator-name: option<string>, 69 + template: build-task-template 70 + } 71 + 72 + /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any 73 + /// debug-adapter-specific configuration options). 74 + record debug-scenario { 75 + /// Unsubstituted label for the task.DebugAdapterBinary 76 + label: string, 77 + /// Name of the Debug Adapter this configuration is intended for. 78 + adapter: string, 79 + /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug. 80 + build: option<build-task-definition>, 81 + /// JSON-encoded configuration for a given debug adapter. 82 + config: string, 83 + /// TCP connection parameters (if they were specified by user) 84 + tcp-connection: option<tcp-arguments-template>, 85 + } 86 + 87 + enum start-debugging-request-arguments-request { 88 + launch, 89 + attach, 90 + } 91 + 92 + record debug-task-definition { 93 + /// Unsubstituted label for the task.DebugAdapterBinary 94 + label: string, 95 + /// Name of the Debug Adapter this configuration is intended for. 96 + adapter: string, 97 + /// JSON-encoded configuration for a given debug adapter. 98 + config: string, 99 + /// TCP connection parameters (if they were specified by user) 100 + tcp-connection: option<tcp-arguments-template>, 101 + } 102 + 103 + record start-debugging-request-arguments { 104 + /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter. 105 + /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter. 106 + configuration: string, 107 + request: start-debugging-request-arguments-request, 108 + } 109 + 110 + /// The lowest-level representation of a debug session, which specifies: 111 + /// - How to start a debug adapter process 112 + /// - How to start a debug session with it (using DAP protocol) 113 + /// for a given debug scenario. 114 + record debug-adapter-binary { 115 + command: option<string>, 116 + arguments: list<string>, 117 + envs: env-vars, 118 + cwd: option<string>, 119 + /// Zed will use TCP transport if `connection` is specified. 120 + connection: option<tcp-arguments>, 121 + request-args: start-debugging-request-arguments 122 + } 123 + }
+167
compiler-core/wit/zed_extension/0.7.0/extension.wit
··· 1 + package zed:extension; 2 + 3 + world extension { 4 + import context-server; 5 + import dap; 6 + import github; 7 + import http-client; 8 + import platform; 9 + import process; 10 + import nodejs; 11 + 12 + use common.{env-vars, range}; 13 + use context-server.{context-server-configuration}; 14 + use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request}; 15 + use lsp.{completion, symbol}; 16 + use process.{command}; 17 + use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; 18 + 19 + /// Initializes the extension. 20 + export init-extension: func(); 21 + 22 + /// The type of a downloaded file. 23 + enum downloaded-file-type { 24 + /// A gzipped file (`.gz`). 25 + gzip, 26 + /// A gzipped tar archive (`.tar.gz`). 27 + gzip-tar, 28 + /// A ZIP file (`.zip`). 29 + zip, 30 + /// An uncompressed file. 31 + uncompressed, 32 + } 33 + 34 + /// The installation status for a language server. 35 + variant language-server-installation-status { 36 + /// The language server has no installation status. 37 + none, 38 + /// The language server is being downloaded. 39 + downloading, 40 + /// The language server is checking for updates. 41 + checking-for-update, 42 + /// The language server installation failed for specified reason. 43 + failed(string), 44 + } 45 + 46 + record settings-location { 47 + worktree-id: u64, 48 + path: string, 49 + } 50 + 51 + import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>; 52 + 53 + /// Downloads a file from the given URL and saves it to the given path within the extension's 54 + /// working directory. 55 + /// 56 + /// The file will be extracted according to the given file type. 57 + import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; 58 + 59 + /// Makes the file at the given path executable. 60 + import make-file-executable: func(filepath: string) -> result<_, string>; 61 + 62 + /// Updates the installation status for the given language server. 63 + import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); 64 + 65 + /// A Zed worktree. 66 + resource worktree { 67 + /// Returns the ID of the worktree. 68 + id: func() -> u64; 69 + /// Returns the root path of the worktree. 70 + root-path: func() -> string; 71 + /// Returns the textual contents of the specified file in the worktree. 72 + read-text-file: func(path: string) -> result<string, string>; 73 + /// Returns the path to the given binary name, if one is present on the `$PATH`. 74 + which: func(binary-name: string) -> option<string>; 75 + /// Returns the current shell environment. 76 + shell-env: func() -> env-vars; 77 + } 78 + 79 + /// A Zed project. 80 + resource project { 81 + /// Returns the IDs of all of the worktrees in this project. 82 + worktree-ids: func() -> list<u64>; 83 + } 84 + 85 + /// A key-value store. 86 + resource key-value-store { 87 + /// Inserts an entry under the specified key. 88 + insert: func(key: string, value: string) -> result<_, string>; 89 + } 90 + 91 + /// Returns the command used to start up the language server. 92 + export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>; 93 + 94 + /// Returns the initialization options to pass to the language server on startup. 95 + /// 96 + /// The initialization options are represented as a JSON string. 97 + export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>; 98 + 99 + /// Returns the workspace configuration options to pass to the language server. 100 + export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>; 101 + 102 + /// Returns the initialization options to pass to the other language server. 103 + export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>; 104 + 105 + /// Returns the workspace configuration options to pass to the other language server. 106 + export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>; 107 + 108 + /// A label containing some code. 109 + record code-label { 110 + /// The source code to parse with Tree-sitter. 111 + code: string, 112 + /// The spans to display in the label. 113 + spans: list<code-label-span>, 114 + /// The range of the displayed label to include when filtering. 115 + filter-range: range, 116 + } 117 + 118 + /// A span within a code label. 119 + variant code-label-span { 120 + /// A range into the parsed code. 121 + code-range(range), 122 + /// A span containing a code literal. 123 + literal(code-label-span-literal), 124 + } 125 + 126 + /// A span containing a code literal. 127 + record code-label-span-literal { 128 + /// The literal text. 129 + text: string, 130 + /// The name of the highlight to use for this literal. 131 + highlight-name: option<string>, 132 + } 133 + 134 + export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>; 135 + export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>; 136 + 137 + 138 + /// Returns the completions that should be shown when completing the provided slash command with the given query. 139 + export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>; 140 + 141 + /// Returns the output from running the provided slash command. 142 + export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>; 143 + 144 + /// Returns the command used to start up a context server. 145 + export context-server-command: func(context-server-id: string, project: borrow<project>) -> result<command, string>; 146 + 147 + /// Returns the configuration for a context server. 148 + export context-server-configuration: func(context-server-id: string, project: borrow<project>) -> result<option<context-server-configuration>, string>; 149 + 150 + /// Returns a list of packages as suggestions to be included in the `/docs` 151 + /// search results. 152 + /// 153 + /// This can be used to provide completions for known packages (e.g., from the 154 + /// local project or a registry) before a package has been indexed. 155 + export suggest-docs-packages: func(provider-name: string) -> result<list<string>, string>; 156 + 157 + /// Indexes the docs for the specified package. 158 + export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>; 159 + 160 + /// Returns a configured debug adapter binary for a given debug task. 161 + export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>; 162 + /// Returns the kind of a debug scenario (launch or attach). 163 + export dap-request-kind: func(adapter-name: string, config: string) -> result<start-debugging-request-arguments-request, string>; 164 + export dap-config-to-scenario: func(config: debug-config) -> result<debug-scenario, string>; 165 + export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option<debug-scenario>; 166 + export run-dap-locator: func(locator-name: string, config: resolved-task) -> result<debug-request, string>; 167 + }
+35
compiler-core/wit/zed_extension/0.7.0/github.wit
··· 1 + interface github { 2 + /// A GitHub release. 3 + record github-release { 4 + /// The version of the release. 5 + version: string, 6 + /// The list of assets attached to the release. 7 + assets: list<github-release-asset>, 8 + } 9 + 10 + /// An asset from a GitHub release. 11 + record github-release-asset { 12 + /// The name of the asset. 13 + name: string, 14 + /// The download URL for the asset. 15 + download-url: string, 16 + } 17 + 18 + /// The options used to filter down GitHub releases. 19 + record github-release-options { 20 + /// Whether releases without assets should be included. 21 + require-assets: bool, 22 + /// Whether pre-releases should be included. 23 + pre-release: bool, 24 + } 25 + 26 + /// Returns the latest release for the given GitHub repository. 27 + /// 28 + /// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed". 29 + latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>; 30 + 31 + /// Returns the GitHub release with the specified tag name for the given GitHub repository. 32 + /// 33 + /// Returns an error if a release with the given tag name does not exist. 34 + github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>; 35 + }
+67
compiler-core/wit/zed_extension/0.7.0/http-client.wit
··· 1 + interface http-client { 2 + /// An HTTP request. 3 + record http-request { 4 + /// The HTTP method for the request. 5 + method: http-method, 6 + /// The URL to which the request should be made. 7 + url: string, 8 + /// The headers for the request. 9 + headers: list<tuple<string, string>>, 10 + /// The request body. 11 + body: option<list<u8>>, 12 + /// The policy to use for redirects. 13 + redirect-policy: redirect-policy, 14 + } 15 + 16 + /// HTTP methods. 17 + enum http-method { 18 + /// `GET` 19 + get, 20 + /// `HEAD` 21 + head, 22 + /// `POST` 23 + post, 24 + /// `PUT` 25 + put, 26 + /// `DELETE` 27 + delete, 28 + /// `OPTIONS` 29 + options, 30 + /// `PATCH` 31 + patch, 32 + } 33 + 34 + /// The policy for dealing with redirects received from the server. 35 + variant redirect-policy { 36 + /// Redirects from the server will not be followed. 37 + /// 38 + /// This is the default behavior. 39 + no-follow, 40 + /// Redirects from the server will be followed up to the specified limit. 41 + follow-limit(u32), 42 + /// All redirects from the server will be followed. 43 + follow-all, 44 + } 45 + 46 + /// An HTTP response. 47 + record http-response { 48 + /// The response headers. 49 + headers: list<tuple<string, string>>, 50 + /// The response body. 51 + body: list<u8>, 52 + } 53 + 54 + /// Performs an HTTP request and returns the response. 55 + fetch: func(req: http-request) -> result<http-response, string>; 56 + 57 + /// An HTTP response stream. 58 + resource http-response-stream { 59 + /// Retrieves the next chunk of data from the response stream. 60 + /// 61 + /// Returns `Ok(None)` if the stream has ended. 62 + next-chunk: func() -> result<option<list<u8>>, string>; 63 + } 64 + 65 + /// Performs an HTTP request and returns a response stream. 66 + fetch-stream: func(req: http-request) -> result<http-response-stream, string>; 67 + }
+90
compiler-core/wit/zed_extension/0.7.0/lsp.wit
··· 1 + interface lsp { 2 + /// An LSP completion. 3 + record completion { 4 + label: string, 5 + label-details: option<completion-label-details>, 6 + detail: option<string>, 7 + kind: option<completion-kind>, 8 + insert-text-format: option<insert-text-format>, 9 + } 10 + 11 + /// The kind of an LSP completion. 12 + variant completion-kind { 13 + text, 14 + method, 15 + function, 16 + %constructor, 17 + field, 18 + variable, 19 + class, 20 + %interface, 21 + module, 22 + property, 23 + unit, 24 + value, 25 + %enum, 26 + keyword, 27 + snippet, 28 + color, 29 + file, 30 + reference, 31 + folder, 32 + enum-member, 33 + constant, 34 + struct, 35 + event, 36 + operator, 37 + type-parameter, 38 + other(s32), 39 + } 40 + 41 + /// Label details for an LSP completion. 42 + record completion-label-details { 43 + detail: option<string>, 44 + description: option<string>, 45 + } 46 + 47 + /// Defines how to interpret the insert text in a completion item. 48 + variant insert-text-format { 49 + plain-text, 50 + snippet, 51 + other(s32), 52 + } 53 + 54 + /// An LSP symbol. 55 + record symbol { 56 + kind: symbol-kind, 57 + name: string, 58 + } 59 + 60 + /// The kind of an LSP symbol. 61 + variant symbol-kind { 62 + file, 63 + module, 64 + namespace, 65 + %package, 66 + class, 67 + method, 68 + property, 69 + field, 70 + %constructor, 71 + %enum, 72 + %interface, 73 + function, 74 + variable, 75 + constant, 76 + %string, 77 + number, 78 + boolean, 79 + array, 80 + object, 81 + key, 82 + null, 83 + enum-member, 84 + struct, 85 + event, 86 + operator, 87 + type-parameter, 88 + other(s32), 89 + } 90 + }
+13
compiler-core/wit/zed_extension/0.7.0/nodejs.wit
··· 1 + interface nodejs { 2 + /// Returns the path to the Node binary used by Zed. 3 + node-binary-path: func() -> result<string, string>; 4 + 5 + /// Returns the latest version of the given NPM package. 6 + npm-package-latest-version: func(package-name: string) -> result<string, string>; 7 + 8 + /// Returns the installed version of the given NPM package, if it exists. 9 + npm-package-installed-version: func(package-name: string) -> result<option<string>, string>; 10 + 11 + /// Installs the specified NPM package. 12 + npm-install-package: func(package-name: string, version: string) -> result<_, string>; 13 + }
+24
compiler-core/wit/zed_extension/0.7.0/platform.wit
··· 1 + interface platform { 2 + /// An operating system. 3 + enum os { 4 + /// macOS. 5 + mac, 6 + /// Linux. 7 + linux, 8 + /// Windows. 9 + windows, 10 + } 11 + 12 + /// A platform architecture. 13 + enum architecture { 14 + /// AArch64 (e.g., Apple Silicon). 15 + aarch64, 16 + /// x86. 17 + x86, 18 + /// x86-64. 19 + x8664, 20 + } 21 + 22 + /// Gets the current operating system and architecture. 23 + current-platform: func() -> tuple<os, architecture>; 24 + }
+29
compiler-core/wit/zed_extension/0.7.0/process.wit
··· 1 + interface process { 2 + use common.{env-vars}; 3 + 4 + /// A command. 5 + record command { 6 + /// The command to execute. 7 + command: string, 8 + /// The arguments to pass to the command. 9 + args: list<string>, 10 + /// The environment variables to set for the command. 11 + env: env-vars, 12 + } 13 + 14 + /// The output of a finished process. 15 + record output { 16 + /// The status (exit code) of the process. 17 + /// 18 + /// On Unix, this will be `None` if the process was terminated by a signal. 19 + status: option<s32>, 20 + /// The data that the process wrote to stdout. 21 + stdout: list<u8>, 22 + /// The data that the process wrote to stderr. 23 + stderr: list<u8>, 24 + } 25 + 26 + /// Executes the given command as a child process, waiting for it to finish 27 + /// and collecting all of its output. 28 + run-command: func(command: command) -> result<output, string>; 29 + }
+41
compiler-core/wit/zed_extension/0.7.0/slash-command.wit
··· 1 + interface slash-command { 2 + use common.{range}; 3 + 4 + /// A slash command for use in the Assistant. 5 + record slash-command { 6 + /// The name of the slash command. 7 + name: string, 8 + /// The description of the slash command. 9 + description: string, 10 + /// The tooltip text to display for the run button. 11 + tooltip-text: string, 12 + /// Whether this slash command requires an argument. 13 + requires-argument: bool, 14 + } 15 + 16 + /// The output of a slash command. 17 + record slash-command-output { 18 + /// The text produced by the slash command. 19 + text: string, 20 + /// The list of sections to show in the slash command placeholder. 21 + sections: list<slash-command-output-section>, 22 + } 23 + 24 + /// A section in the slash command output. 25 + record slash-command-output-section { 26 + /// The range this section occupies. 27 + range: range, 28 + /// The label to display in the placeholder for this section. 29 + label: string, 30 + } 31 + 32 + /// A completion for a slash command argument. 33 + record slash-command-argument-completion { 34 + /// The label to display for this completion. 35 + label: string, 36 + /// The new text that should be inserted into the command when this completion is accepted. 37 + new-text: string, 38 + /// Whether the command should be run when accepting this completion. 39 + run-command: bool, 40 + } 41 + }
+79
docs/compiler/wasm-zed-extensions.md
··· 1 + # Wasm Zed extensions 2 + 3 + Gleam can build [Zed](https://zed.dev) extensions as WebAssembly **components** 4 + that implement the full `zed:extension` world (API **0.7.0**). 5 + 6 + This repository is the **compiler**. Real extension packages (e.g. Glint) live 7 + with the language project, not here. 8 + 9 + **Installable Glint extension:** `/home/nandi/code/glint/editors/zed` 10 + 11 + ## Author experience 12 + 13 + ```text 14 + my_ext/ 15 + gleam.toml # target = "wasm" 16 + extension.toml # languages, grammars, language_servers 17 + languages/… 18 + src/extension.gleam 19 + ``` 20 + 21 + ```gleam 22 + pub type Worktree 23 + pub type Command { 24 + Command(command: String, args: List(String), env: List(#(String, String))) 25 + } 26 + 27 + pub fn language_server_command( 28 + _id: String, 29 + worktree: Worktree, 30 + ) -> Result(Command, String) { 31 + // … 32 + } 33 + ``` 34 + 35 + ```sh 36 + gleam export zed-extension 37 + # → extension.wasm (component + zed:api-version) 38 + ``` 39 + 40 + ## What the compiler emits 41 + 42 + 1. **Gleam → MIR → core Wasm** for user modules (heap strings, structs, 43 + lists, `@external(wasm, …)` imports). 44 + 2. **Guest shell** for every `zed:extension` world export: 45 + - Real `language-server-command` (PATH `which` + documented fallback) 46 + - Defaults for other callbacks (`Err("not implemented")` / empty results) 47 + - `cabi_realloc` bump allocator and post-return no-ops 48 + 3. **Component packaging** via `wit-component` with vendored WIT 49 + (`compiler-core/wit/zed_extension/0.7.0/`). 50 + 4. Custom section **`zed:api-version`**: six bytes, big-endian `u16` major / 51 + minor / patch for `0.7.0`. 52 + 53 + ## Zed Install Dev Extension (today) 54 + 55 + Zed always runs `cargo build --target wasm32-wasip2` when 56 + `[lib] kind = "Rust"`. It does **not** install a prebuilt Gleam 57 + `extension.wasm` alone. Until that changes, installable extensions still 58 + need a thin Rust guest (as in `glint/editors/zed`). 59 + 60 + Grammar keys must match the tree-sitter C symbol (e.g. `grammars.gleam` → 61 + `tree_sitter_gleam`). 62 + 63 + ## Example package (out of tree) 64 + 65 + | Location | Role | 66 + |----------|------| 67 + | `~/code/glint/editors/zed` | Installable Glint extension (Rust guest + Gleam sketch) | 68 + | `~/code/gleam` (this repo) | Compiler: MIR, CABI, `gleam export zed-extension` | 69 + | `examples/hello_wasm` | Core Wasm smoke test (not a Zed extension) | 70 + 71 + ## Status / roadmap 72 + 73 + | Piece | Status | 74 + |-------|--------| 75 + | Full world exports + component | Done | 76 + | `language-server-command` CABI | Done (guest shell) | 77 + | `@external(wasm)` + structs/lists | Partial | 78 + | Wire Gleam body into CABI exports | In progress | 79 + | Zed prebuilt install (no cargo) | Needs Zed change or alternate install path |