diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 751b8df..c7f5f0d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -9,6 +9,7 @@ pub mod project; pub mod resolve; pub mod runtime; pub mod source; +pub mod stdlib; pub mod target; pub mod types; pub mod wasm; diff --git a/crates/core/src/stdlib.rs b/crates/core/src/stdlib.rs new file mode 100644 index 0000000..4c60a5e --- /dev/null +++ b/crates/core/src/stdlib.rs @@ -0,0 +1,420 @@ +use std::collections::HashMap; + +use crate::{ + source::{SourceFileId, Span}, + types::{ConstructorInfo, FieldInfo, ModuleInterface, Type, TypeDeclaration}, +}; + +const STDLIB_SPAN: Span = Span { file_id: SourceFileId(u32::MAX), start: 0, end: 0 }; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModuleStrategy { + Hybrid, + PreferCompiledSource, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemberStrategy { + Intrinsic, + HostImport, + ManagedConstructor, + InterfaceOnly, + PreferCompiledSource, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StdlibMember { + pub name: &'static str, + pub strategy: MemberStrategy, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StdlibModule { + pub name: &'static str, + pub strategy: ModuleStrategy, + pub interface: ModuleInterface, + pub members: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StdlibRegistry { + modules: HashMap<&'static str, StdlibModule>, +} + +impl StdlibRegistry { + pub fn new() -> Self { + Self { + modules: stdlib_modules() + .into_iter() + .map(|module| (module.name, module)) + .collect(), + } + } + + pub fn module(&self, name: &str) -> Option<&StdlibModule> { + self.modules.get(name) + } + + pub fn interface(&self, name: &str) -> Option<&ModuleInterface> { + self.module(name).map(|module| &module.interface) + } + + pub fn member_strategy(&self, module: &str, member: &str) -> Option { + self.module(module)? + .members + .iter() + .find(|entry| entry.name == member) + .map(|entry| entry.strategy) + } + + pub fn modules(&self) -> impl Iterator { + self.modules.values() + } +} + +impl Default for StdlibRegistry { + fn default() -> Self { + Self::new() + } +} + +fn stdlib_modules() -> Vec { + vec![ + StdlibModule::gleam_io(), + StdlibModule::gleam_int(), + StdlibModule::gleam_string(), + StdlibModule::gleam_list(), + StdlibModule::gleam_result(), + StdlibModule::gleam_option(), + StdlibModule::gleam_order(), + StdlibModule::remaining("gleam/bit_array"), + StdlibModule::remaining("gleam/bool"), + StdlibModule::remaining("gleam/bytes_tree"), + StdlibModule::remaining("gleam/dict"), + StdlibModule::remaining("gleam/dynamic"), + StdlibModule::remaining("gleam/dynamic/decode"), + StdlibModule::remaining("gleam/float"), + StdlibModule::remaining("gleam/function"), + StdlibModule::remaining("gleam/pair"), + StdlibModule::remaining("gleam/set"), + StdlibModule::remaining("gleam/string_tree"), + StdlibModule::remaining("gleam/uri"), + ] +} + +impl StdlibModule { + fn gleam_io() -> Self { + Self::new( + "gleam/io", + ModuleStrategy::Hybrid, + &[ + function("println", vec![Type::String], Type::Nil, MemberStrategy::HostImport), + function("print", vec![Type::String], Type::Nil, MemberStrategy::HostImport), + function( + "debug", + vec![Type::generic("a")], + Type::generic("a"), + MemberStrategy::HostImport, + ), + ], + &[], + ) + } + + fn gleam_int() -> Self { + Self::new( + "gleam/int", + ModuleStrategy::Hybrid, + &[ + function("to_string", vec![Type::Int], Type::String, MemberStrategy::Intrinsic), + function( + "parse", + vec![Type::String], + result(Type::Int, Type::Nil), + MemberStrategy::InterfaceOnly, + ), + ], + &[], + ) + } + + fn gleam_string() -> Self { + Self::new( + "gleam/string", + ModuleStrategy::Hybrid, + &[ + function( + "append", + vec![Type::String, Type::String], + Type::String, + MemberStrategy::Intrinsic, + ), + function( + "concat", + vec![Type::List(Box::new(Type::String))], + Type::String, + MemberStrategy::Intrinsic, + ), + function("length", vec![Type::String], Type::Int, MemberStrategy::Intrinsic), + function("is_empty", vec![Type::String], Type::Bool, MemberStrategy::Intrinsic), + ], + &[], + ) + } + + fn gleam_list() -> Self { + Self::new( + "gleam/list", + ModuleStrategy::Hybrid, + &[ + function( + "length", + vec![Type::List(Box::new(Type::generic("a")))], + Type::Int, + MemberStrategy::Intrinsic, + ), + function( + "reverse", + vec![Type::List(Box::new(Type::generic("a")))], + list(Type::generic("a")), + MemberStrategy::Intrinsic, + ), + function( + "map", + vec![ + list(Type::generic("a")), + fn_type(vec![Type::generic("a")], Type::generic("b")), + ], + list(Type::generic("b")), + MemberStrategy::InterfaceOnly, + ), + function( + "fold", + vec![ + list(Type::generic("a")), + Type::generic("b"), + fn_type(vec![Type::generic("b"), Type::generic("a")], Type::generic("b")), + ], + Type::generic("b"), + MemberStrategy::InterfaceOnly, + ), + ], + &[], + ) + } + + fn gleam_result() -> Self { + let result_type = type_decl( + "Result", + vec!["a", "e"], + false, + vec![ + constructor( + "Ok", + vec![field("value", Type::generic("a"))], + result(Type::generic("a"), Type::generic("e")), + ), + constructor( + "Error", + vec![field("reason", Type::generic("e"))], + result(Type::generic("a"), Type::generic("e")), + ), + ], + ); + Self::new( + "gleam/result", + ModuleStrategy::Hybrid, + &[ + constructor_member("Ok"), + constructor_member("Error"), + function( + "map", + vec![ + result(Type::generic("a"), Type::generic("e")), + fn_type(vec![Type::generic("a")], Type::generic("b")), + ], + result(Type::generic("b"), Type::generic("e")), + MemberStrategy::InterfaceOnly, + ), + ], + &[result_type], + ) + } + + fn gleam_option() -> Self { + let option_type = type_decl( + "Option", + vec!["a"], + false, + vec![ + constructor( + "Some", + vec![field("value", Type::generic("a"))], + option(Type::generic("a")), + ), + constructor("None", vec![], option(Type::generic("a"))), + ], + ); + Self::new( + "gleam/option", + ModuleStrategy::Hybrid, + &[ + constructor_member("Some"), + constructor_member("None"), + function( + "map", + vec![ + option(Type::generic("a")), + fn_type(vec![Type::generic("a")], Type::generic("b")), + ], + option(Type::generic("b")), + MemberStrategy::InterfaceOnly, + ), + ], + &[option_type], + ) + } + + fn gleam_order() -> Self { + let order_type = type_decl( + "Order", + vec![], + false, + vec![ + constructor("Lt", vec![], Type::custom("Order", vec![])), + constructor("Eq", vec![], Type::custom("Order", vec![])), + constructor("Gt", vec![], Type::custom("Order", vec![])), + ], + ); + Self::new( + "gleam/order", + ModuleStrategy::Hybrid, + &[ + constructor_member("Lt"), + constructor_member("Eq"), + constructor_member("Gt"), + ], + &[order_type], + ) + } + + fn remaining(name: &'static str) -> Self { + Self::new(name, ModuleStrategy::PreferCompiledSource, &[], &[]) + } + + fn new( + name: &'static str, strategy: ModuleStrategy, members: &[StdlibMemberSpec], types: &[TypeDeclaration], + ) -> Self { + let mut interface = ModuleInterface::default(); + let mut member_entries = Vec::new(); + + for member in members { + member_entries.push(StdlibMember { name: member.name, strategy: member.strategy }); + if let Some(type_) = &member.type_ { + interface.functions.insert(member.name.into(), type_.clone()); + } + } + + for type_ in types { + interface.types.insert(type_.name.clone(), type_.clone()); + for constructor in &type_.constructors { + interface + .constructors + .insert(constructor.name.clone(), constructor.clone()); + } + } + + Self { name, strategy, interface, members: member_entries } + } +} + +#[derive(Debug, Clone)] +struct StdlibMemberSpec { + name: &'static str, + strategy: MemberStrategy, + type_: Option, +} + +fn function(name: &'static str, params: Vec, return_type: Type, strategy: MemberStrategy) -> StdlibMemberSpec { + StdlibMemberSpec { name, strategy, type_: Some(fn_type(params, return_type)) } +} + +fn constructor_member(name: &'static str) -> StdlibMemberSpec { + StdlibMemberSpec { name, strategy: MemberStrategy::ManagedConstructor, type_: None } +} + +fn type_decl(name: &str, parameters: Vec<&str>, opaque: bool, constructors: Vec) -> TypeDeclaration { + TypeDeclaration { + name: name.into(), + parameters: parameters.into_iter().map(str::to_string).collect(), + opaque, + constructors, + span: STDLIB_SPAN, + } +} + +fn constructor(name: &str, fields: Vec, return_type: Type) -> ConstructorInfo { + ConstructorInfo { name: name.into(), fields, return_type, span: STDLIB_SPAN } +} + +fn field(name: &str, type_: Type) -> FieldInfo { + FieldInfo { name: name.into(), type_ } +} + +fn fn_type(params: Vec, return_type: Type) -> Type { + Type::Function { params, return_type: Box::new(return_type) } +} + +fn list(item: Type) -> Type { + Type::List(Box::new(item)) +} + +fn result(ok: Type, error: Type) -> Type { + Type::custom("Result", vec![ok, error]) +} + +fn option(item: Type) -> Type { + Type::custom("Option", vec![item]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registers_initial_modules_and_member_strategies() { + let registry = StdlibRegistry::new(); + + assert_eq!( + registry.member_strategy("gleam/io", "println"), + Some(MemberStrategy::HostImport), + ); + assert_eq!( + registry.member_strategy("gleam/int", "to_string"), + Some(MemberStrategy::Intrinsic), + ); + assert_eq!( + registry.member_strategy("gleam/result", "Ok"), + Some(MemberStrategy::ManagedConstructor), + ); + } + + #[test] + fn exposes_initial_interfaces() { + let registry = StdlibRegistry::new(); + let interface = registry.interface("gleam/string").expect("string interface"); + + assert_eq!( + interface.functions.get("append"), + Some(&fn_type(vec![Type::String, Type::String], Type::String)), + ); + } + + #[test] + fn records_remaining_stdlib_strategy() { + let registry = StdlibRegistry::new(); + let module = registry.module("gleam/dict").expect("dict module"); + + assert_eq!(module.strategy, ModuleStrategy::PreferCompiledSource); + } +} diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index c9cc148..80daba0 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1892,6 +1892,14 @@ impl Type { parse_type_source(source) } + pub fn custom(name: impl Into, args: Vec) -> Self { + Self::Custom { name: name.into(), args } + } + + pub fn generic(name: impl Into) -> Self { + Self::Generic(name.into()) + } + fn substitute_from(&self, actual: &Type) -> Type { let substitutions = substitutions_for(self, actual); substitute_type(self, &substitutions) diff --git a/docs/src/internal/specs/14_stdlib_and_host_interop.md b/docs/src/internal/specs/14_stdlib_and_host_interop.md index 3ac29c4..0b2b230 100644 --- a/docs/src/internal/specs/14_stdlib_and_host_interop.md +++ b/docs/src/internal/specs/14_stdlib_and_host_interop.md @@ -15,6 +15,65 @@ Possible standard library strategies include: The strategy can vary by module, but it should be explicit and tested. +Regulus should support the stdlib in two groups. + +### Group 1: initial useful stdlib + +Group 1 is the small stdlib surface needed for examples and tests. These +modules should be visible to resolver and type checker through the same module +interface path as project modules. Members that are not implemented yet should +still fail with source-spanned unsupported diagnostics, not during WASM +assembly. + +The group contains: + +- `gleam/io` +- `gleam/int` +- `gleam/string` +- `gleam/list` +- `gleam/result` +- `gleam/option` +- `gleam/order` + +Initial implementations should cover: + +| Module | Initial support | +| --- | --- | +| `gleam/io` | `println`, `print`, maybe `debug` | +| `gleam/int` | `to_string`, later `parse` | +| `gleam/string` | `append`, `concat`, `length`, `is_empty` | +| `gleam/list` | `length`, `reverse`, later `map` and `fold` | +| `gleam/result` | `Result`, `Ok`, `Error`, maybe `map` | +| `gleam/option` | `Option`, `Some`, `None`, maybe `map` | +| `gleam/order` | `Order`, `Lt`, `Eq`, `Gt` | + +Each implemented member should declare one lowering strategy: intrinsic, host +import, compiled Gleam source, or adapter around a host import. + +### Group 2: remaining stdlib + +Group 2 is the rest of the Gleam stdlib. It should be finished after Group 1, +the host ABI, and the target adapters are stable. + +The remaining modules are: + +- `gleam/bit_array` +- `gleam/bool` +- `gleam/bytes_tree` +- `gleam/dict` +- `gleam/dynamic` +- `gleam/dynamic/decode` +- `gleam/float` +- `gleam/function` +- `gleam/pair` +- `gleam/set` +- `gleam/string_tree` +- `gleam/uri` + +Group 2 should prefer compiling stdlib Gleam source where possible. Runtime +intrinsics and host adapters should be reserved for functions that require +special ABI support, target capabilities, or efficient runtime primitives. + ## Host ABI The host ABI should define how values cross the WASM boundary: diff --git a/docs/src/internal/tasks/14_stdlib_and_host_interop.md b/docs/src/internal/tasks/14_stdlib_and_host_interop.md index 389a8e2..208fdf2 100644 --- a/docs/src/internal/tasks/14_stdlib_and_host_interop.md +++ b/docs/src/internal/tasks/14_stdlib_and_host_interop.md @@ -8,9 +8,10 @@ Support useful standard library modules and host calls. ### Standard library strategy -- [ ] Decide which stdlib modules are compiled from Gleam source, shimmed as +- [x] Decide which stdlib modules are compiled from Gleam source, shimmed as host imports, or implemented as compiler/runtime intrinsics. -- [ ] Define a small initial stdlib support set for examples and tests. +- [x] Define a table-driven stdlib registry for module interfaces and lowering + strategies. - [ ] Load or model dependency package metadata needed for stdlib modules. - [ ] Resolve stdlib module interfaces consistently with project module interfaces. @@ -22,6 +23,21 @@ Support useful standard library modules and host calls. - [ ] Add diagnostics for unsupported stdlib modules, functions, types, or target combinations. +### Group 1: initial useful stdlib + +- [ ] Model interfaces for `gleam/io`, `gleam/int`, `gleam/string`, + `gleam/list`, `gleam/result`, `gleam/option`, and `gleam/order`. +- [ ] Implement `gleam/io.println` and `gleam/io.print` as host calls. +- [ ] Implement `gleam/int.to_string` as an intrinsic or runtime helper. +- [ ] Implement `gleam/string.append`, `concat`, `length`, and `is_empty`. +- [ ] Implement `gleam/list.length` and `gleam/list.reverse`. +- [ ] Support `gleam/result.Result`, `Ok`, and `Error` in interfaces and + lowering. +- [ ] Support `gleam/option.Option`, `Some`, and `None` in interfaces and + lowering. +- [ ] Support `gleam/order.Order`, `Lt`, `Eq`, and `Gt` in interfaces and + lowering. + ### Host ABI - [ ] Define concrete host imports and adapters for Wasmtime, browser, and WASI @@ -36,7 +52,6 @@ Support useful standard library modules and host calls. ### Intrinsics and host calls -- [ ] Implement selected stdlib functions needed by examples. - [ ] Implement or import IO functions for Wasmtime tests and browser examples. - [ ] Implement equality, string, bit-array, list, result, option, and debug helpers where needed by compiled programs. @@ -45,6 +60,24 @@ Support useful standard library modules and host calls. - [ ] Add Wasmtime tests for host imported functions. - [ ] Add fixtures using common Gleam stdlib modules. +### Group 2: remaining stdlib + +- [ ] Support `gleam/bit_array`. +- [ ] Support `gleam/bool`. +- [ ] Support `gleam/bytes_tree`. +- [ ] Support `gleam/dict`. +- [ ] Support `gleam/dynamic`. +- [ ] Support `gleam/dynamic/decode`. +- [ ] Support `gleam/float`. +- [ ] Support `gleam/function`. +- [ ] Support `gleam/pair`. +- [ ] Support `gleam/set`. +- [ ] Support `gleam/string_tree`. +- [ ] Support `gleam/uri`. +- [ ] Prefer compiling stdlib Gleam source for Group 2 where possible. +- [ ] Add target-specific intrinsics or host adapters only when source + compilation is not enough. + ## Done when Small programs using selected Gleam stdlib functionality compile and execute