5454//! Some(x) ⟹ inr(x)
5555//! ```
5656
57- use std:: collections:: HashMap ;
57+ use std:: collections:: { HashMap , HashSet } ;
5858
5959use ephapax_surface:: {
6060 ConstructorDef , DataDecl , MatchArm , Pattern , Span , SurfaceDecl , SurfaceExpr , SurfaceExprKind ,
@@ -71,6 +71,28 @@ fn lower_visibility(v: SurfaceVisibility) -> Visibility {
7171 }
7272}
7373
74+ /// The qualifier name of a `base.member` access, if `base` is a bare
75+ /// module name — a `Var(m)` (lower-case module) or a nullary
76+ /// `Construct { ctor: m }` (upper-case module). Anything else (a value,
77+ /// a call result, …) has no module qualifier.
78+ fn module_qualifier_of ( base : & SurfaceExpr ) -> Option < SmolStr > {
79+ match & base. kind {
80+ SurfaceExprKind :: Var ( v) => Some ( v. clone ( ) ) ,
81+ SurfaceExprKind :: Construct { ctor, args } if args. is_empty ( ) => Some ( ctor. clone ( ) ) ,
82+ _ => None ,
83+ }
84+ }
85+
86+ /// The last `/`- or `.`-separated segment of an import path
87+ /// (`lib/math` → `math`, `Foo.Bar` → `Bar`, `Coproc` → `Coproc`). This is
88+ /// the name a `M.member` qualifier must use.
89+ fn last_segment ( path : & str ) -> SmolStr {
90+ path. rsplit ( |c| c == '/' || c == '.' )
91+ . next ( )
92+ . unwrap_or ( path)
93+ . into ( )
94+ }
95+
7496/// Desugaring errors.
7597#[ derive( Debug , Clone , Error ) ]
7698pub enum DesugarError {
@@ -99,6 +121,12 @@ pub enum DesugarError {
99121
100122 #[ error( "empty match expression" ) ]
101123 EmptyMatch ,
124+
125+ #[ error( "qualified access `{qualifier}.{member}`: `{qualifier}` is not an imported module (record field access is not yet supported)" ) ]
126+ QualifiedAccessUnknownModule { qualifier : String , member : String } ,
127+
128+ #[ error( "member access on a non-module expression is not supported (only `Module.member` qualified access)" ) ]
129+ FieldAccessOnNonModule ,
102130}
103131
104132// =========================================================================
@@ -217,26 +245,44 @@ impl DataRegistry {
217245/// [`DataRegistry`] and uses it to transform `Construct` and `Match` nodes.
218246pub struct Desugarer {
219247 registry : DataRegistry ,
248+ /// Last-segment names of the importer's modules (e.g. `math` for
249+ /// `import lib/math`), used to recognise a `M.member` qualifier as a
250+ /// module reference at desugar time. Reset per-module by
251+ /// [`Desugarer::desugar_module`].
252+ module_qualifiers : HashSet < SmolStr > ,
220253}
221254
222255impl Desugarer {
223256 /// Create a new desugarer with an empty data registry.
224257 pub fn new ( ) -> Self {
225258 Self {
226259 registry : DataRegistry :: new ( ) ,
260+ module_qualifiers : HashSet :: new ( ) ,
227261 }
228262 }
229263
230264 /// Create a new desugarer with a pre-populated registry.
231265 pub fn with_registry ( registry : DataRegistry ) -> Self {
232- Self { registry }
266+ Self {
267+ registry,
268+ module_qualifiers : HashSet :: new ( ) ,
269+ }
233270 }
234271
235272 /// Desugar a complete surface module to a core module.
236273 ///
237274 /// First pass: collect all data declarations into the registry.
238275 /// Second pass: desugar all declarations.
239276 pub fn desugar_module ( & mut self , module : & SurfaceModule ) -> Result < Module , DesugarError > {
277+ // Record this module's import qualifiers (the last path segment of
278+ // each `import`), so qualified `M.member` access resolves against
279+ // them. Reset per module — qualifiers are not inherited.
280+ self . module_qualifiers = module
281+ . imports
282+ . iter ( )
283+ . map ( |i| last_segment ( i. module . as_str ( ) ) )
284+ . collect ( ) ;
285+
240286 // First pass: register all data types and extern types
241287 for decl in & module. decls {
242288 match decl {
@@ -523,11 +569,50 @@ impl Desugarer {
523569 SurfaceExprKind :: Match { scrutinee, arms } => {
524570 return self . desugar_match ( scrutinee, arms, span) ;
525571 }
572+
573+ SurfaceExprKind :: FieldAccess { base, field } => {
574+ return self . desugar_field_access ( base, field, span) ;
575+ }
526576 } ;
527577
528578 Ok ( Expr :: new ( kind, span) )
529579 }
530580
581+ /// Desugar qualified module-member access `M.member`.
582+ ///
583+ /// `M` must be an imported module (matched by the last segment of its
584+ /// import path); phase-I import resolution has already made `M`'s
585+ /// public items visible in the importer's shared scope, so the member
586+ /// resolves *unqualified*: an upper-case `member` is a constructor
587+ /// (`Construct`), otherwise a function / value reference (`Var`).
588+ /// Record field access on a value is not yet supported.
589+ fn desugar_field_access (
590+ & self ,
591+ base : & SurfaceExpr ,
592+ field : & SmolStr ,
593+ span : Span ,
594+ ) -> Result < Expr , DesugarError > {
595+ let qualifier = module_qualifier_of ( base) . ok_or ( DesugarError :: FieldAccessOnNonModule ) ?;
596+ if !self . module_qualifiers . contains ( & qualifier) {
597+ return Err ( DesugarError :: QualifiedAccessUnknownModule {
598+ qualifier : qualifier. to_string ( ) ,
599+ member : field. to_string ( ) ,
600+ } ) ;
601+ }
602+ let resolved = if field. chars ( ) . next ( ) . is_some_and ( |c| c. is_ascii_uppercase ( ) ) {
603+ SurfaceExpr :: new (
604+ SurfaceExprKind :: Construct {
605+ ctor : field. clone ( ) ,
606+ args : Vec :: new ( ) ,
607+ } ,
608+ span,
609+ )
610+ } else {
611+ SurfaceExpr :: new ( SurfaceExprKind :: Var ( field. clone ( ) ) , span)
612+ } ;
613+ self . desugar_expr ( & resolved)
614+ }
615+
531616 // =====================================================================
532617 // Type desugaring
533618 // =====================================================================
@@ -770,7 +855,9 @@ impl Desugarer {
770855 let ( _, ctor_defs) = self
771856 . registry
772857 . get_type_ctors ( info. data_name . as_str ( ) )
773- . ok_or_else ( || DesugarError :: UnknownType { name : info. data_name . to_string ( ) } ) ?;
858+ . ok_or_else ( || DesugarError :: UnknownType {
859+ name : info. data_name . to_string ( ) ,
860+ } ) ?;
774861 let ctors = ctor_defs. clone ( ) ;
775862
776863 // Wrap in inl/inr chain based on index
@@ -935,8 +1022,12 @@ impl Desugarer {
9351022 // Find the data type from constructor patterns
9361023 let data_name = self . find_data_type_from_arms ( arms) ?;
9371024
938- let ( _, ctor_defs) = self . registry . get_type_ctors ( data_name. as_str ( ) )
939- . ok_or_else ( || DesugarError :: UnknownType { name : data_name. to_string ( ) } ) ?;
1025+ let ( _, ctor_defs) = self
1026+ . registry
1027+ . get_type_ctors ( data_name. as_str ( ) )
1028+ . ok_or_else ( || DesugarError :: UnknownType {
1029+ name : data_name. to_string ( ) ,
1030+ } ) ?;
9401031 let ctors = ctor_defs. clone ( ) ;
9411032
9421033 // Build an ordered map: constructor index → (pattern bindings, body)
@@ -1856,15 +1947,24 @@ mod tests {
18561947 } ;
18571948 // The two fn items end up at indexes 2 and 3 (after the two
18581949 // type items at 0 and 1).
1859- let ExternItem :: Fn { ret_ty : window_ret, .. } = & items[ 2 ] else {
1950+ let ExternItem :: Fn {
1951+ ret_ty : window_ret, ..
1952+ } = & items[ 2 ]
1953+ else {
18601954 panic ! ( "expected ExternItem::Fn at index 2" ) ;
18611955 } ;
1862- let ExternItem :: Fn { ret_ty : ipc_ret, .. } = & items[ 3 ] else {
1956+ let ExternItem :: Fn {
1957+ ret_ty : ipc_ret, ..
1958+ } = & items[ 3 ]
1959+ else {
18631960 panic ! ( "expected ExternItem::Fn at index 3" ) ;
18641961 } ;
18651962 assert_eq ! ( * window_ret, Ty :: Var ( "Window" . into( ) ) ) ;
18661963 assert_eq ! ( * ipc_ret, Ty :: Var ( "IpcChannel" . into( ) ) ) ;
1867- assert_ne ! ( window_ret, ipc_ret, "distinct nominal types must not be equal" ) ;
1964+ assert_ne ! (
1965+ window_ret, ipc_ret,
1966+ "distinct nominal types must not be equal"
1967+ ) ;
18681968 }
18691969
18701970 /// `SurfaceDecl::Extern` lowers to `Decl::Extern` with item types
@@ -1903,7 +2003,12 @@ mod tests {
19032003 & items[ 0 ] ,
19042004 ExternItem :: Type { name } if name. as_str( ) == "Window"
19052005 ) ) ;
1906- if let ExternItem :: Fn { name, params, ret_ty } = & items[ 1 ] {
2006+ if let ExternItem :: Fn {
2007+ name,
2008+ params,
2009+ ret_ty,
2010+ } = & items[ 1 ]
2011+ {
19072012 assert_eq ! ( name. as_str( ) , "window_open" ) ;
19082013 assert_eq ! ( params. len( ) , 1 ) ;
19092014 assert_eq ! ( params[ 0 ] . 0 . as_str( ) , "title" ) ;
0 commit comments