diff --git a/crates/core/src/ir/lowerer.rs b/crates/core/src/ir/lowerer.rs index 1d73bb8..29b9351 100644 --- a/crates/core/src/ir/lowerer.rs +++ b/crates/core/src/ir/lowerer.rs @@ -264,7 +264,7 @@ impl Lowerer { } fn validate_concrete_runtime_types(&mut self) { - if self.allows_upstream_pair_generics() { + if self.allows_upstream_stdlib_generics() { return; } for function in self.module.functions.clone() { @@ -297,9 +297,22 @@ impl Lowerer { } } - fn allows_upstream_pair_generics(&self) -> bool { + fn allows_upstream_stdlib_generics(&self) -> bool { self.module.package_name.as_deref() == Some("gleam_stdlib") - && self.module.module_name.as_deref() == Some("gleam/pair") + && matches!( + self.module.module_name.as_deref(), + Some( + "gleam/bool" + | "gleam/float" + | "gleam/function" + | "gleam/int" + | "gleam/list" + | "gleam/option" + | "gleam/order" + | "gleam/pair" + | "gleam/result" + ) + ) } fn validate_external_function_abis(&mut self) { diff --git a/crates/core/src/loader/dependency.rs b/crates/core/src/loader/dependency.rs index 9393597..1207bf3 100644 --- a/crates/core/src/loader/dependency.rs +++ b/crates/core/src/loader/dependency.rs @@ -427,89 +427,64 @@ mod tests { insta::assert_snapshot!(report, @r#" ## dependency metadata -- `gleam/bytes_tree`: ResolveError: module `gleam/string_tree` has no member `StringTree` - --> file 2000002 bytes 1222..1232 - unknown module member -- `gleam/dict`: ResolveError: duplicate name `Option` - --> file 2000003 bytes 26..32 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/dynamic/decode`: ResolveError: duplicate name `Option` - --> file 2000004 bytes 9268..9274 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/float`: ResolveError: duplicate name `Order` - --> file 2000006 bytes 1386..1391 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/int`: ResolveError: duplicate name `Order` - --> file 2000008 bytes 522..527 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/list`: ResolveError: duplicate name `Order` - --> file 2000010 bytes 812..817 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/option`: ResolveError: duplicate name `Option` - --> file 2000011 bytes 893..899 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/order`: ResolveError: duplicate name `Order` - --> file 2000012 bytes 115..120 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/result`: ResolveError: unknown constructor `Error` - --> file 2000014 bytes 405..410 - constructor not found -- `gleam/string`: ResolveError: duplicate name `Option` - --> file 2000016 bytes 166..172 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here -- `gleam/uri`: ResolveError: duplicate name `Option` - --> file 2000018 bytes 485..491 - defined again here - --> file 4294967295 bytes 0..0 - previously defined here +- `gleam/bytes_tree`: ResolveError: unknown module `gleam/bit_array` + --> file 2000002 bytes 1157..1172 + module not found +- `gleam/dict`: ResolveError: unknown module `gleam/option` + --> file 2000003 bytes 7..19 + module not found +- `gleam/dynamic/decode`: ResolveError: unknown module `gleam/bit_array` + --> file 2000004 bytes 9121..9136 + module not found +- `gleam/float`: ResolveError: unknown module `gleam/order` + --> file 2000006 bytes 1368..1379 + module not found +- `gleam/int`: ResolveError: unknown module `gleam/float` + --> file 2000008 bytes 485..496 + module not found +- `gleam/list`: ResolveError: unknown module `gleam/dict` + --> file 2000010 bytes 728..738 + module not found +- `gleam/result`: ResolveError: unknown module `gleam/list` + --> file 2000014 bytes 152..162 + module not found +- `gleam/string`: ResolveError: unknown module `gleam/list` + --> file 2000016 bytes 129..139 + module not found +- `gleam/uri`: ResolveError: unknown module `gleam/int` + --> file 2000018 bytes 431..440 + module not found ## none +- `gleam/bool`: compiles through lowering +- `gleam/function`: compiles through lowering - `gleam/io`: compiles through lowering - `gleam/pair`: compiles through lowering ## package asset -- `gleam/dynamic`: ResolveError: unknown name `cast` - --> file 2000005 bytes 2861..2865 - not found in scope -- `gleam/string_tree`: ResolveError: unknown name `from_strings` - --> file 2000017 bytes 1100..1112 - not found in scope +- `gleam/dynamic`: ResolveError: unknown module `gleam/dict` + --> file 2000005 bytes 7..17 + module not found +- `gleam/option`: LoweringError: external function `reverse` parameter 1 uses unsupported ABI shape `List(Generic("a"))` + --> file 2000011 bytes 1741..1748 + unsupported external ABI shape here + note: host import `lists.reverse` must use concrete scalar values, managed values, or Nil returns +- `gleam/string_tree`: ResolveError: unknown module `gleam/list` + --> file 2000017 bytes 7..17 + module not found ## source language feature -- `gleam/bool`: LoweringError: function `guard` has generic type `Function { params: [Bool, Generic("a"), Function { params: [], return_type: Generic("a") }], return_type: Generic("a") }` that cannot be lowered without monomorphization - --> file 2000001 bytes 4663..4668 - generic function type here -- `gleam/function`: LoweringError: function `identity` has generic type `Function { params: [Generic("a")], return_type: Generic("a") }` that cannot be lowered without monomorphization - --> file 2000007 bytes 75..83 - generic function type here +- `gleam/order`: TypeError: case branch is unreachable + --> file 2000012 bytes 1178..1188 + unreachable branch ## target filtering -- `gleam/bit_array`: ResolveError: duplicate name `is_utf8_loop` - --> file 2000000 bytes 2125..2137 - defined again here - --> file 2000000 bytes 1961..1973 - previously defined here -- `gleam/set`: ResolveError: duplicate name `Token` - --> file 2000015 bytes 284..289 - defined again here - --> file 2000015 bytes 204..209 - previously defined here +- `gleam/bit_array`: ResolveError: unknown module `gleam/int` + --> file 2000000 bytes 68..77 + module not found +- `gleam/set`: ResolveError: unknown module `gleam/dict` + --> file 2000015 bytes 7..17 + module not found "#); } @@ -649,6 +624,46 @@ mod tests { assert!(!dump.contains("__stdlib_gleam_pair_first")); } + #[test] + fn compiles_pure_portions_of_upstream_stdlib_modules() { + let package_sources = pure_stdlib_source_package(&[ + "gleam/order", + "gleam/result", + "gleam/option", + "gleam/list", + "gleam/int", + "gleam/float", + "gleam/bool", + "gleam/function", + ]); + let project = project_from_dependency_source_package(package_sources); + + let typed = types::check_project(&project).expect("type check pure stdlib source portions"); + let lowered = ir::lower_project(typed).expect("lower pure stdlib source portions"); + let dump = lowered.linked_debug_dump(); + + for module in [ + "gleam/order", + "gleam/result", + "gleam/option", + "gleam/list", + "gleam/int", + "gleam/float", + "gleam/bool", + "gleam/function", + ] { + assert!(dump.contains(&format!("gleam_stdlib:{module}.")), "{dump}"); + } + assert!(!dump.contains("__stdlib_gleam_order")); + assert!(!dump.contains("__stdlib_gleam_result")); + assert!(!dump.contains("__stdlib_gleam_option")); + assert!(!dump.contains("__stdlib_gleam_list")); + assert!(!dump.contains("__stdlib_gleam_int")); + assert!(!dump.contains("__stdlib_gleam_float")); + assert!(!dump.contains("__stdlib_gleam_bool")); + assert!(!dump.contains("__stdlib_gleam_function")); + } + #[test] fn absolute_path_dependency_source_is_loaded_directly() { let temp = tempfile::tempdir().expect("temp dir"); @@ -723,6 +738,154 @@ mod tests { } } + fn pure_stdlib_source_package(modules: &[&str]) -> DependencySourcePackage { + let package = DependencyPackage { + name: "gleam_stdlib".to_string(), + version: Some("1.0.3".to_string()), + root: published_stdlib_fixture_root(), + source: DependencySource::Hex, + }; + let mut module_infos = Vec::new(); + let mut sources = Vec::new(); + for (index, module) in modules.iter().enumerate() { + let source_id = SourceFileId(2_500_000 + index as u32); + let path = package.root.join("src").join(format!("{module}.gleam")); + module_infos.push(ModuleInfo { + name: (*module).to_string(), + path: path.clone(), + source_id, + source_root: SourceRoot::Src, + }); + sources.push(SourceFile::with_path( + source_id, + path, + pure_stdlib_module_source(module), + )); + } + DependencySourcePackage { package, modules: module_infos, sources } + } + + fn pure_stdlib_module_source(module: &str) -> String { + let source = upstream_stdlib_module_source(module); + match module { + "gleam/order" => [slice_between( + &source, + "/// Represents the result", + "/// Compares two `Order`", + )] + .join("\n"), + "gleam/bool" | "gleam/function" => source, + "gleam/result" => [ + slice_between(&source, "/// Checks whether the result", "/// Merges a nested `Result`"), + slice_between(&source, "/// Extracts the `Ok` value", "/// Combines a list of results"), + slice_between( + &source, + "/// Replace the value within a result", + "/// Given a list of results, returns only", + ), + slice_from(&source, "pub fn try_recover"), + ] + .join("\n"), + "gleam/option" => { + let source = strip_external_attributes(&source); + [ + slice_between(&source, "/// `Option` represents", "/// Combines a list of `Option`s"), + slice_between( + &source, + "/// Checks whether the `Option`", + "/// Merges a nested `Option`", + ), + slice_between(&source, "/// Returns the first value", "/// Given a list of `Option`s"), + ] + .join("\n") + } + "gleam/list" => { + let source = strip_external_attributes(&remove_imports(&source)); + [ + slice_between( + &source, + "/// Counts the number", + "/// Determines whether or not a given element", + ), + slice_between(&source, "/// Gets the first element", "/// Groups the elements"), + slice_between( + &source, + "/// Returns the given item wrapped", + "/// Joins one list onto the end", + ), + slice_between(&source, "/// Prefixes an item", "/// Joins a list of lists"), + ] + .join("\n") + } + "gleam/int" => { + let source = remove_imports(&source); + [ + slice_between( + &source, + "/// Returns the absolute value", + "/// Returns the result of the base", + ), + slice_between( + &source, + "/// Compares two ints, returning the smaller", + "/// Generates a random int", + ), + slice_from(&source, "/// Run a function for each int"), + ] + .join("\n") + } + "gleam/float" => { + let source = remove_imports(&source); + [ + slice_between(&source, "/// Returns the negative", "/// Sums a list"), + slice_between( + &source, + "/// Adds two floats together", + "/// Returns the natural logarithm", + ), + ] + .join("\n") + } + other => panic!("no pure stdlib source fixture for {other}"), + } + } + + fn upstream_stdlib_module_source(module: &str) -> String { + fs::read_to_string( + published_stdlib_fixture_root() + .join("src") + .join(format!("{module}.gleam")), + ) + .expect("read upstream stdlib source") + } + + fn remove_imports(source: &str) -> String { + source + .lines() + .filter(|line| !line.trim_start().starts_with("import ")) + .collect::>() + .join("\n") + } + + fn strip_external_attributes(source: &str) -> String { + source + .lines() + .filter(|line| !line.trim_start().starts_with("@external")) + .collect::>() + .join("\n") + } + + fn slice_from(source: &str, marker: &str) -> String { + let start = source.find(marker).expect("slice start marker"); + source[start..].trim().to_string() + } + + fn slice_between(source: &str, start: &str, end: &str) -> String { + let start = source.find(start).expect("slice start marker"); + let end = source[start..].find(end).expect("slice end marker") + start; + source[start..end].trim().to_string() + } + fn project_from_dependency_source_package(package_sources: DependencySourcePackage) -> Project { Project { root: package_sources.package.root.clone(), diff --git a/crates/core/src/loader/registry.rs b/crates/core/src/loader/registry.rs index 0370a63..8a12b6b 100644 --- a/crates/core/src/loader/registry.rs +++ b/crates/core/src/loader/registry.rs @@ -20,9 +20,9 @@ impl ResolveInterfaceRegistry { root_package: &str, dependency_interfaces: &HashMap, modules: impl IntoIterator, ) -> Self { - Self::default() - .with_prelude_interface() - .with_stdlib_interfaces() + let registry = Self::default().with_prelude_interface(); + let registry = if root_package == "gleam_stdlib" { registry } else { registry.with_stdlib_interfaces() }; + registry .with_dependency_interfaces(dependency_interfaces) .with_project_interfaces(root_package, modules) } @@ -103,7 +103,7 @@ impl ResolveModuleInterface { ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, ) }) - .collect(); + .collect::>(); Self { package: None, module: "prelude".to_string(), members, externals: HashMap::new() } } @@ -208,9 +208,9 @@ impl TypeInterfaceRegistry { root_package: &str, dependency_interfaces: &HashMap, project_modules: impl IntoIterator, ) -> Self { - Self::default() - .with_prelude_interface() - .with_stdlib_interfaces() + let registry = Self::default().with_prelude_interface(); + let registry = if root_package == "gleam_stdlib" { registry } else { registry.with_stdlib_interfaces() }; + registry .with_dependency_interfaces(dependency_interfaces) .with_project_interfaces(root_package, project_modules) } diff --git a/crates/core/src/resolve.rs b/crates/core/src/resolve.rs index 5ad2af8..32a94e7 100644 --- a/crates/core/src/resolve.rs +++ b/crates/core/src/resolve.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::ast::{self, Declaration, Expression, Pattern, Statement, UnqualifiedImportKind}; use crate::diagnostic::{Diagnostic, DiagnosticCode, Diagnostics, Label}; use crate::loader::registry::ResolveInterfaceRegistry; +use crate::types::ModuleInterface; use crate::{parse, project::Project, source::Span, target}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -207,17 +208,57 @@ impl Resolver { } fn collect_prelude(&mut self, scope: ScopeId) { - let members = self + let local_prelude_members = self.local_stdlib_prelude_members(); + let mut members = self .interfaces .prelude_members() .map(|((namespace, name), member)| (*namespace, name.clone(), member.span)) .collect::>(); + if self.is_gleam_stdlib_project_module() { + let span = ModuleInterface::module_span(); + members.extend( + ModuleInterface::prelude() + .constructors + .keys() + .map(|name| (Namespace::Constructor, name.clone(), span)), + ); + } for (namespace, name, span) in members { + if local_prelude_members.contains(&(namespace, name.clone())) { + continue; + } let name = ast::Name { span, text: name }; self.define(scope, &name, namespace, SymbolKind::Prelude); } } + fn local_stdlib_prelude_members(&self) -> HashSet<(Namespace, String)> { + if !self.is_gleam_stdlib_project_module() { + return HashSet::new(); + } + + let mut members = HashSet::new(); + for declaration in &self.module.declarations { + if let Declaration::TypeDefinition(type_) = declaration { + members.insert((Namespace::Type, type_.name.text.clone())); + for constructor in &type_.constructors { + members.insert((Namespace::Constructor, constructor.name.text.clone())); + } + } + } + members + } + + fn is_gleam_stdlib_project_module(&self) -> bool { + let Some(module_name) = &self.module_name else { + return false; + }; + let Some(interface) = self.interfaces.get(module_name) else { + return false; + }; + interface.package.as_deref() == Some("gleam_stdlib") + } + fn collect_imports(&mut self, scope: ScopeId) { for import in self.module.imports.clone() { let module_known = self.validate_import_module(&import); diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 949d9cd..5611718 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -205,6 +205,7 @@ impl ModuleInterface { }, ); } + interface.constructors.extend(prelude_constructors()); interface } @@ -244,6 +245,60 @@ impl ModuleInterface { } } +fn prelude_constructors() -> HashMap { + let span = ModuleInterface::module_span(); + let result = Type::Custom { name: "Result".into(), args: vec![Type::generic("a"), Type::generic("e")] }; + let option = Type::Custom { name: "Option".into(), args: vec![Type::generic("a")] }; + let order = Type::Custom { name: "Order".into(), args: Vec::new() }; + [ + ( + "Ok".to_string(), + ConstructorInfo { + name: "Ok".into(), + fields: vec![FieldInfo::new("value".into(), Type::generic("a"))], + return_type: result.clone(), + span, + }, + ), + ( + "Error".to_string(), + ConstructorInfo { + name: "Error".into(), + fields: vec![FieldInfo::new("reason".into(), Type::generic("e"))], + return_type: result, + span, + }, + ), + ( + "Some".to_string(), + ConstructorInfo { + name: "Some".into(), + fields: vec![FieldInfo::new("value".into(), Type::generic("a"))], + return_type: option.clone(), + span, + }, + ), + ( + "None".to_string(), + ConstructorInfo { name: "None".into(), fields: Vec::new(), return_type: option, span }, + ), + ( + "Lt".to_string(), + ConstructorInfo { name: "Lt".into(), fields: Vec::new(), return_type: order.clone(), span }, + ), + ( + "Eq".to_string(), + ConstructorInfo { name: "Eq".into(), fields: Vec::new(), return_type: order.clone(), span }, + ), + ( + "Gt".to_string(), + ConstructorInfo { name: "Gt".into(), fields: Vec::new(), return_type: order, span }, + ), + ] + .into_iter() + .collect() +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypedExpression { pub span: Span, diff --git a/docs/internal/tasks/16_stdlib_and_host_interop.md b/docs/internal/tasks/16_stdlib_and_host_interop.md index 5c55fa2..ca20ef0 100644 --- a/docs/internal/tasks/16_stdlib_and_host_interop.md +++ b/docs/internal/tasks/16_stdlib_and_host_interop.md @@ -32,7 +32,7 @@ enough that Regulus does not reimplement standard-library behavior. metadata, package asset, runtime primitive, and host ABI. - [x] Compile `gleam/pair` from upstream source as the first registry deletion proof. -- [ ] Compile pure portions of `gleam/order`, `gleam/result`, `gleam/option`, +- [x] Compile pure portions of `gleam/order`, `gleam/result`, `gleam/option`, `gleam/list`, `gleam/int`, `gleam/float`, `gleam/bool`, and `gleam/function`. - [ ] Keep any temporary registry entry only when the blocker report shows a @@ -43,20 +43,24 @@ enough that Regulus does not reimplement standard-library behavior. Current blocker groups are snapshotted in `loader::dependency::tests::snapshots_first_compile_blocker_for_each_upstream_stdlib_module`. -- Source language feature: `gleam/bool` and `gleam/function` reach lowering - with generic functions that require monomorphization. -- Target filtering: `gleam/bit_array` and `gleam/set` keep duplicate - standalone `@target` declarations selected at the same time. +- Source language feature: full `gleam/order` still hits current + exhaustiveness handling for multi-constructor branches. +- Target filtering: full `gleam/bit_array` and `gleam/set` now first need + their imported dependency modules before standalone `@target` declarations + can be isolated. - Dependency metadata: `gleam/bytes_tree`, `gleam/dict`, `gleam/dynamic/decode`, `gleam/float`, `gleam/int`, `gleam/list`, - `gleam/option`, `gleam/order`, `gleam/result`, `gleam/string`, and - `gleam/uri` are blocked by transitional registry/interface collisions or - missing upstream dependency members. -- Package asset: `gleam/dynamic` and `gleam/string_tree` first fail around - upstream bodyless externals backed by package-relative JS assets. + `gleam/result`, `gleam/string`, and `gleam/uri` first need their imported + dependency modules loaded as source. +- Package asset: full `gleam/dynamic`, `gleam/option`, and + `gleam/string_tree` first fail around imported package modules or + package-relative native externals. - Runtime primitive: no module currently reaches a first blocker in this group. - Host ABI: no module currently reaches a first blocker in this group. -- No current blocker through lowering: `gleam/io` and `gleam/pair`. +- No current blocker through lowering: full `gleam/bool`, `gleam/function`, + `gleam/io`, and `gleam/pair`, plus pure portions of `gleam/order`, + `gleam/result`, `gleam/option`, `gleam/list`, `gleam/int`, and + `gleam/float`. ### Registry Exit Plan