Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ephapax-linear/src/affine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ impl AffineChecker {
}
}
Decl::Type { .. } | Decl::Const { .. } => {}
// Extern declarations have no body — no affine discipline
// applies. The signatures only exist to inform codegen.
Decl::Extern { .. } => {}
}
}

Expand Down
3 changes: 3 additions & 0 deletions ephapax-linear/src/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ impl LinearChecker {
}
}
Decl::Type { .. } | Decl::Const { .. } => { /* no discipline check needed */ }
// Extern declarations have no body — no linear discipline
// applies. The signatures only exist to inform codegen.
Decl::Extern { .. } => {}
}
}

Expand Down
191 changes: 185 additions & 6 deletions src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ use std::collections::HashMap;

use ephapax_surface::{
ConstructorDef, DataDecl, MatchArm, Pattern, Span, SurfaceDecl, SurfaceExpr, SurfaceExprKind,
SurfaceModule, SurfaceTy,
SurfaceExternItem, SurfaceModule, SurfaceTy,
};
use ephapax_syntax::{BaseTy, Decl, Expr, ExprKind, Literal, Module, Ty, Visibility};
use ephapax_syntax::{BaseTy, Decl, Expr, ExprKind, ExternItem, Literal, Module, Ty, Visibility};
use smol_str::SmolStr;
use thiserror::Error;

Expand Down Expand Up @@ -122,6 +122,15 @@ pub struct DataRegistry {
constructors: HashMap<SmolStr, ConstructorInfo>,
/// Data type name → (params, constructors)
types: HashMap<SmolStr, (Vec<SmolStr>, Vec<ConstructorDef>)>,
/// Names declared via `extern "abi" { type Foo }` blocks.
///
/// Extern types are nominal and opaque: the desugar pass resolves
/// `SurfaceTy::Named { name: "Foo", args: [] }` to `Ty::Var("Foo")`
/// rather than emitting an `UnknownType` error. The type checker
/// then treats them as rigid type variables that only unify with
/// themselves (so values of type `Window` cannot be confused with
/// `IpcChannel`, etc.).
extern_types: std::collections::HashSet<SmolStr>,
}

impl DataRegistry {
Expand Down Expand Up @@ -151,6 +160,12 @@ impl DataRegistry {
);
}

/// Register an opaque extern type name (from
/// `extern "abi" { type Foo }`).
pub fn register_extern_type(&mut self, name: &SmolStr) {
self.extern_types.insert(name.clone());
}

/// Look up a constructor by name.
fn get_ctor(&self, name: &str) -> Option<&ConstructorInfo> {
self.constructors.get(name)
Expand All @@ -160,6 +175,11 @@ impl DataRegistry {
fn get_type_ctors(&self, name: &str) -> Option<&(Vec<SmolStr>, Vec<ConstructorDef>)> {
self.types.get(name)
}

/// Is this name a registered extern (opaque) type?
fn is_extern_type(&self, name: &str) -> bool {
self.extern_types.contains(name)
}
}

// =========================================================================
Expand Down Expand Up @@ -193,10 +213,20 @@ impl Desugarer {
/// First pass: collect all data declarations into the registry.
/// Second pass: desugar all declarations.
pub fn desugar_module(&mut self, module: &SurfaceModule) -> Result<Module, DesugarError> {
// First pass: register all data types
// First pass: register all data types and extern types
for decl in &module.decls {
if let SurfaceDecl::Data(data) = decl {
self.registry.register(data);
match decl {
SurfaceDecl::Data(data) => {
self.registry.register(data);
}
SurfaceDecl::Extern { items, .. } => {
for item in items {
if let SurfaceExternItem::Type { name } = item {
self.registry.register_extern_type(name);
}
}
}
_ => {}
}
}

Expand Down Expand Up @@ -236,6 +266,39 @@ impl Desugarer {
// as core declarations. The type information lives in the
// registry and is used to desugar Construct/Match nodes.
SurfaceDecl::Data(_) => {}
// Extern blocks lower their items' surface types to core
// types and keep them as `Decl::Extern`. The type checker
// and codegen pick them up from there. No body to desugar.
SurfaceDecl::Extern { abi, items, .. } => {
let mut core_items: Vec<ExternItem> = Vec::with_capacity(items.len());
for item in items {
match item {
SurfaceExternItem::Type { name } => {
core_items.push(ExternItem::Type { name: name.clone() });
}
SurfaceExternItem::Fn {
name,
params,
ret_ty,
} => {
let core_params: Vec<(SmolStr, Ty)> = params
.iter()
.map(|(n, t)| Ok((n.clone(), self.desugar_ty(t)?)))
.collect::<Result<_, DesugarError>>()?;
let core_ret = self.desugar_ty(ret_ty)?;
core_items.push(ExternItem::Fn {
name: name.clone(),
params: core_params,
ret_ty: core_ret,
});
}
}
}
core_decls.push(Decl::Extern {
abi: abi.clone(),
items: core_items,
});
}
}
}

Expand Down Expand Up @@ -469,7 +532,18 @@ impl Desugarer {
///
/// `Option(I32)` → `() + I32`
/// `Result(I32, Bool)` → `I32 + Bool`
///
/// **Extern types** (registered via `extern "abi" { type Foo }`) are
/// nominal and opaque — they resolve to `Ty::Var(name)` regardless
/// of any args. Type arguments on extern names are accepted but
/// ignored: extern types currently have no parameter abstraction
/// (parameterised externs are a phase-2C concern, post wasm
/// codegen). Two distinct extern types `Window` and `IpcChannel`
/// produce two distinct `Ty::Var` rigids that never unify.
fn desugar_named_type(&self, name: &SmolStr, args: &[SurfaceTy]) -> Result<Ty, DesugarError> {
if self.registry.is_extern_type(name.as_str()) {
return Ok(Ty::Var(name.clone()));
}
let (params, ctors) = self.registry.get_type_ctors(name.as_str()).ok_or_else(|| {
DesugarError::UnknownType {
name: name.to_string(),
Expand Down Expand Up @@ -1023,7 +1097,7 @@ mod tests {
use super::*;
use ephapax_surface::{
BaseTy, ConstructorDef, DataDecl, Literal, MatchArm, Pattern, Span, SurfaceDecl,
SurfaceExpr, SurfaceExprKind, SurfaceModule, SurfaceTy,
SurfaceExpr, SurfaceExprKind, SurfaceExternItem, SurfaceModule, SurfaceTy,
};

fn se(kind: SurfaceExprKind) -> SurfaceExpr {
Expand Down Expand Up @@ -1580,4 +1654,109 @@ mod tests {
}
}
}

/// An extern type declared inside `extern "abi" { type Foo }`
/// becomes a nominal opaque `Ty::Var("Foo")` when referenced from
/// an extern fn's signature. Two distinct extern types produce
/// two distinct `Ty::Var` names so the type checker keeps them
/// apart.
#[test]
fn desugar_extern_types_resolve_to_ty_var() {
let module = SurfaceModule {
name: "test".into(),
decls: vec![SurfaceDecl::Extern {
abi: "gossamer".to_string(),
items: vec![
SurfaceExternItem::Type {
name: "Window".into(),
},
SurfaceExternItem::Type {
name: "IpcChannel".into(),
},
SurfaceExternItem::Fn {
name: "open_window".into(),
params: vec![],
ret_ty: SurfaceTy::Named {
name: "Window".into(),
args: vec![],
},
},
SurfaceExternItem::Fn {
name: "open_ipc".into(),
params: vec![],
ret_ty: SurfaceTy::Named {
name: "IpcChannel".into(),
args: vec![],
},
},
],
span: Span::dummy(),
}],
};

let core = desugar(&module).unwrap();
assert_eq!(core.decls.len(), 1);
let Decl::Extern { items, .. } = &core.decls[0] else {
panic!("expected Decl::Extern");
};
// The two fn items end up at indexes 2 and 3 (after the two
// type items at 0 and 1).
let ExternItem::Fn { ret_ty: window_ret, .. } = &items[2] else {
panic!("expected ExternItem::Fn at index 2");
};
let ExternItem::Fn { ret_ty: ipc_ret, .. } = &items[3] else {
panic!("expected ExternItem::Fn at index 3");
};
assert_eq!(*window_ret, Ty::Var("Window".into()));
assert_eq!(*ipc_ret, Ty::Var("IpcChannel".into()));
assert_ne!(window_ret, ipc_ret, "distinct nominal types must not be equal");
}

/// `SurfaceDecl::Extern` lowers to `Decl::Extern` with item types
/// converted from `SurfaceTy` to core `Ty`. Phase 2A: the
/// declaration survives desugar intact; downstream typecheck and
/// codegen accept it but currently treat it as a no-op (phase 2B
/// will wire those).
#[test]
fn desugar_extern_block() {
let module = SurfaceModule {
name: "test".into(),
decls: vec![SurfaceDecl::Extern {
abi: "gossamer".to_string(),
items: vec![
SurfaceExternItem::Type {
name: "Window".into(),
},
SurfaceExternItem::Fn {
name: "window_open".into(),
params: vec![("title".into(), SurfaceTy::String("r".into()))],
ret_ty: SurfaceTy::Base(BaseTy::I32),
},
],
span: Span::dummy(),
}],
};

let core = desugar(&module).unwrap();
assert_eq!(core.decls.len(), 1);
match &core.decls[0] {
Decl::Extern { abi, items } => {
assert_eq!(abi, "gossamer");
assert_eq!(items.len(), 2);
assert!(matches!(
&items[0],
ExternItem::Type { name } if name.as_str() == "Window"
));
if let ExternItem::Fn { name, params, ret_ty } = &items[1] {
assert_eq!(name.as_str(), "window_open");
assert_eq!(params.len(), 1);
assert_eq!(params[0].0.as_str(), "title");
assert_eq!(*ret_ty, Ty::Base(BaseTy::I32));
} else {
panic!("expected ExternItem::Fn, got {:?}", &items[1]);
}
}
other => panic!("expected Decl::Extern, got {other:?}"),
}
}
}
39 changes: 39 additions & 0 deletions src/ephapax-ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,45 @@ fn decl_to_sexpr(decl: &Decl) -> SExpr {
ty.as_ref().map(ty_to_sexpr).unwrap_or(SExpr::Atom("_".into())),
expr_to_sexpr(value),
]),
// Render `extern "abi" { ... }` as a tagged s-expr so the IR
// round-trip preserves the declaration. Item shape mirrors the
// wasm view that codegen will eventually consume:
// (extern "abi" ((extern-type Foo) (extern-fn name ((p T)...) R)))
Decl::Extern { abi, items } => SExpr::List(vec![
SExpr::Atom("extern".into()),
SExpr::Atom(format!("{abi:?}")),
SExpr::List(
items
.iter()
.map(|item| match item {
ephapax_syntax::ExternItem::Type { name } => SExpr::List(vec![
SExpr::Atom("extern-type".into()),
SExpr::Atom(escape_atom(name)),
]),
ephapax_syntax::ExternItem::Fn {
name,
params,
ret_ty,
} => SExpr::List(vec![
SExpr::Atom("extern-fn".into()),
SExpr::Atom(escape_atom(name)),
SExpr::List(
params
.iter()
.map(|(p, t)| {
SExpr::List(vec![
SExpr::Atom(escape_atom(p)),
ty_to_sexpr(t),
])
})
.collect(),
),
ty_to_sexpr(ret_ty),
]),
})
.collect(),
),
]),
}
}

Expand Down
20 changes: 13 additions & 7 deletions src/ephapax-lsp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ fn extract_declarations(module: &Module, _source: &str) -> Vec<DeclInfo> {
module
.decls
.iter()
.map(|decl| match decl {
.filter_map(|decl| match decl {
Decl::Fn {
name,
params,
Expand All @@ -508,24 +508,24 @@ fn extract_declarations(module: &Module, _source: &str) -> Vec<DeclInfo> {
format_ty(ret_ty)
);

DeclInfo {
Some(DeclInfo {
name: name.to_string(),
kind: DeclKind::Function,
span: body.span,
signature: sig,
params: param_strs,
return_type: Some(format_ty(ret_ty)),
}
})
}
Decl::Type { name, visibility: _, ty } => DeclInfo {
Decl::Type { name, visibility: _, ty } => Some(DeclInfo {
name: name.to_string(),
kind: DeclKind::TypeAlias,
span: Span::dummy(),
signature: format!("type {} = {}", name, format_ty(ty)),
params: Vec::new(),
return_type: None,
},
Decl::Const { name, ty, value } => DeclInfo {
}),
Decl::Const { name, ty, value } => Some(DeclInfo {
name: name.to_string(),
kind: DeclKind::TypeAlias, // closest existing variant
span: value.span,
Expand All @@ -536,7 +536,13 @@ fn extract_declarations(module: &Module, _source: &str) -> Vec<DeclInfo> {
),
params: Vec::new(),
return_type: ty.as_ref().map(|t| format_ty(t)),
},
}),
// TODO(ephapax#43 phase 2B): expose extern items as
// navigable LSP symbols. For phase 2A the LSP simply
// doesn't index extern declarations; the block parses
// and lives in the AST but isn't visible to hover /
// go-to-definition yet.
Decl::Extern { .. } => None,
})
.collect()
}
Expand Down
Loading