feat: support module-qualified constructor patterns in match expressions (issue 3)

Added module: Option<Ident> to Pattern::Constructor, updated parser to
handle module.Constructor(args) syntax in patterns, exported ADT
constructors from modules, and copied type definitions during module
import so types like Shape are usable in importing files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 09:46:51 -05:00
parent caabaeeb9c
commit 1fc472a54c
6 changed files with 70 additions and 11 deletions

View File

@@ -697,8 +697,9 @@ pub enum Pattern {
Var(Ident), Var(Ident),
/// Literal: 42, "hello", true /// Literal: 42, "hello", true
Literal(Literal), Literal(Literal),
/// Constructor: Some(x), None, Ok(v) /// Constructor: Some(x), None, Ok(v), module.Constructor(x)
Constructor { Constructor {
module: Option<Ident>,
name: Ident, name: Ident,
fields: Vec<Pattern>, fields: Vec<Pattern>,
span: Span, span: Span,

View File

@@ -333,11 +333,13 @@ mod tests {
fn test_option_exhaustive() { fn test_option_exhaustive() {
let patterns = vec![ let patterns = vec![
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("None"), name: make_ident("None"),
fields: vec![], fields: vec![],
span: span(), span: span(),
}, },
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Some"), name: make_ident("Some"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
@@ -352,6 +354,7 @@ mod tests {
#[test] #[test]
fn test_option_missing_none() { fn test_option_missing_none() {
let patterns = vec![Pattern::Constructor { let patterns = vec![Pattern::Constructor {
module: None,
name: make_ident("Some"), name: make_ident("Some"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
@@ -391,11 +394,13 @@ mod tests {
fn test_result_exhaustive() { fn test_result_exhaustive() {
let patterns = vec![ let patterns = vec![
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Ok"), name: make_ident("Ok"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
}, },
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Err"), name: make_ident("Err"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),

View File

@@ -772,12 +772,22 @@ impl Formatter {
Pattern::Wildcard(_) => "_".to_string(), Pattern::Wildcard(_) => "_".to_string(),
Pattern::Var(ident) => ident.name.clone(), Pattern::Var(ident) => ident.name.clone(),
Pattern::Literal(lit) => self.format_literal(lit), Pattern::Literal(lit) => self.format_literal(lit),
Pattern::Constructor { name, fields, .. } => { Pattern::Constructor {
module,
name,
fields,
..
} => {
let prefix = match module {
Some(m) => format!("{}.", m.name),
None => String::new(),
};
if fields.is_empty() { if fields.is_empty() {
name.name.clone() format!("{}{}", prefix, name.name)
} else { } else {
format!( format!(
"{}({})", "{}{}({})",
prefix,
name.name, name.name,
fields fields
.iter() .iter()

View File

@@ -279,6 +279,12 @@ impl ModuleLoader {
} }
Declaration::Type(t) if t.visibility == Visibility::Public => { Declaration::Type(t) if t.visibility == Visibility::Public => {
exports.insert(t.name.name.clone()); exports.insert(t.name.name.clone());
// Also export constructors for ADT types
if let crate::ast::TypeDef::Enum(variants) = &t.definition {
for variant in variants {
exports.insert(variant.name.name.clone());
}
}
} }
Declaration::Effect(e) => { Declaration::Effect(e) => {
// Effects are always exported // Effects are always exported

View File

@@ -1922,9 +1922,27 @@ impl Parser {
TokenKind::Ident(name) => { TokenKind::Ident(name) => {
// Check if it starts with uppercase (constructor) or lowercase (variable) // Check if it starts with uppercase (constructor) or lowercase (variable)
if name.chars().next().map_or(false, |c| c.is_uppercase()) { if name.chars().next().map_or(false, |c| c.is_uppercase()) {
self.parse_constructor_pattern() self.parse_constructor_pattern_with_module(None)
} else { } else {
let ident = self.parse_ident()?; let ident = self.parse_ident()?;
// Check for module-qualified constructor: module.Constructor
if self.check(TokenKind::Dot) {
// Peek ahead to see if next is an uppercase identifier
let dot_pos = self.pos;
self.advance(); // skip dot
if let TokenKind::Ident(next_name) = self.peek_kind() {
if next_name
.chars()
.next()
.map_or(false, |c| c.is_uppercase())
{
return self
.parse_constructor_pattern_with_module(Some(ident));
}
}
// Not a module-qualified constructor, backtrack
self.pos = dot_pos;
}
Ok(Pattern::Var(ident)) Ok(Pattern::Var(ident))
} }
} }
@@ -1934,8 +1952,14 @@ impl Parser {
} }
} }
fn parse_constructor_pattern(&mut self) -> Result<Pattern, ParseError> { fn parse_constructor_pattern_with_module(
let start = self.current_span(); &mut self,
module: Option<Ident>,
) -> Result<Pattern, ParseError> {
let start = module
.as_ref()
.map(|m| m.span)
.unwrap_or_else(|| self.current_span());
let name = self.parse_ident()?; let name = self.parse_ident()?;
if self.check(TokenKind::LParen) { if self.check(TokenKind::LParen) {
@@ -1952,10 +1976,16 @@ impl Parser {
} }
self.expect(TokenKind::RParen)?; self.expect(TokenKind::RParen)?;
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
Ok(Pattern::Constructor { name, fields, span })
} else {
let span = name.span;
Ok(Pattern::Constructor { Ok(Pattern::Constructor {
module,
name,
fields,
span,
})
} else {
let span = start.merge(name.span);
Ok(Pattern::Constructor {
module,
name, name,
fields: Vec::new(), fields: Vec::new(),
span, span,

View File

@@ -981,6 +981,13 @@ impl TypeChecker {
if !fields.is_empty() { if !fields.is_empty() {
self.env.bind(&name, TypeScheme::mono(Type::Record(fields))); self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
} }
// Also copy type definitions so imported types are usable
for (type_name, type_def) in &module_checker.env.types {
if !self.env.types.contains_key(type_name) {
self.env.types.insert(type_name.clone(), type_def.clone());
}
}
} }
ImportKind::Direct => { ImportKind::Direct => {
// Import a specific name directly // Import a specific name directly
@@ -2476,7 +2483,7 @@ impl TypeChecker {
Vec::new() Vec::new()
} }
Pattern::Constructor { name, fields, span } => { Pattern::Constructor { name, fields, span, .. } => {
// Look up constructor // Look up constructor
// For now, handle Option specially // For now, handle Option specially
match name.name.as_str() { match name.name.as_str() {