use crate::v1::functor;
use crate::v1::surface::{self, ModSurface, SurfaceEnv};
use rustyfi_syntax::cst;
use rustyfi_syntax::cst_v1::{self, ast as ast_v1};
use rustyfi_syntax::leaf::*;
use rustyfi_syntax::Span;
#[derive(Debug, Clone, thiserror::Error)]
#[error(
"{span}: SATySFi 0.1 construct not supported yet in this port's Slice 1: {construct} ({hint})"
)]
pub struct LowerError {
pub construct: &'static str,
pub hint: &'static str,
pub span: Span,
}
fn unsupported(construct: &'static str, hint: &'static str, span: Span) -> LowerError {
LowerError {
construct,
hint,
span,
}
}
struct AbsolutizeRewrite<'a, 's> {
surfaces: &'a SurfaceEnv<'s>,
}
impl functor::HeadRewrite for AbsolutizeRewrite<'_, '_> {
fn rewrite(
&self,
mods: &[String],
path: &[String],
_span: Span,
) -> Result<Option<Vec<String>>, LowerError> {
let Some(head) = mods.first() else {
return Ok(None);
};
let Some((resolved, _)) = surface::resolve_module(self.surfaces, path, head) else {
return Ok(None);
};
if resolved == *head {
return Ok(None);
}
let mut out: Vec<String> = resolved.split('.').map(str::to_string).collect();
out.extend(mods[1..].iter().cloned());
Ok(Some(out))
}
fn rewrite_bare_names(&self) -> bool {
false
}
fn walk_signatures(&self) -> bool {
false
}
fn reject_nested_functor_literals(&self) -> bool {
false
}
}
#[derive(Clone, Default)]
pub(crate) struct TypeNameEnv(std::collections::HashMap<String, String>);
pub(crate) fn qualify_type_key(mod_path: &[String], local: &str) -> String {
if mod_path.is_empty() {
local.to_string()
} else {
format!("{}.{}", mod_path.join("."), local)
}
}
impl TypeNameEnv {
pub(crate) fn qualify(&self, bare: &str) -> String {
self.0
.get(bare)
.cloned()
.unwrap_or_else(|| bare.to_string())
}
pub(crate) fn child<'a, 's>(
&self,
mod_path: &[String],
binds: impl Iterator<Item = &'a cst_v1::Bind>,
surfaces: &SurfaceEnv<'s>,
) -> Self {
let mut map = self.0.clone();
for b in binds {
match b {
cst_v1::Bind::Type { first, ands, .. } => {
map.insert(
first.name.name.clone(),
qualify_type_key(mod_path, &first.name.name),
);
for a in ands {
map.insert(
a.bind.name.name.clone(),
qualify_type_key(mod_path, &a.bind.name.name),
);
}
}
cst_v1::Bind::Include { kw, body } => match &*body.0 {
ast_v1::ModExpr::Var(_) => {
if let Some(Some(target)) =
surface::frozen_include_target(surfaces, mod_path, kw.0)
{
if let Some(target_surf) = surfaces.modules.get(target) {
for (t, _) in &target_surf.types {
map.insert(t.clone(), qualify_type_key(mod_path, t));
}
}
}
}
ast_v1::ModExpr::App { func, arg: _ } => {
let app_span = mod_chain_span(func);
if let Some(Some(surface::AppResolution { functor_path, .. })) =
surface::frozen_app_target(surfaces, mod_path, app_span)
{
if let Some(fdef) = surfaces.functors.get(functor_path) {
if let Some(body_binds) = functor::functor_body_binds(fdef.body) {
for b in body_binds.iter().map(|sb| sb.0.as_ref()) {
if let cst_v1::Bind::Type { first, ands, .. } = b {
map.insert(
first.name.name.clone(),
qualify_type_key(mod_path, &first.name.name),
);
for a in ands {
map.insert(
a.bind.name.name.clone(),
qualify_type_key(mod_path, &a.bind.name.name),
);
}
}
}
}
}
}
}
ast_v1::ModExpr::Struct { .. }
| ast_v1::ModExpr::Coerce { .. }
| ast_v1::ModExpr::Functor { .. } => {}
},
_ => {}
}
}
TypeNameEnv(map)
}
pub(crate) fn child_from_names(
&self,
mod_path: &[String],
names: impl Iterator<Item = String>,
) -> Self {
let mut map = self.0.clone();
for n in names {
let q = qualify_type_key(mod_path, &n);
map.insert(n, q);
}
TypeNameEnv(map)
}
}
pub fn lower_file_v1(file: &cst_v1::FileV1) -> Result<Vec<cst::TopBinding>, LowerError> {
let mut surfaces = SurfaceEnv::default();
surface::build_file_surface(file, &mut surfaces);
lower_file_v1_with_surfaces(file, &surfaces)
}
pub(crate) fn lower_file_v1_with_surfaces<'a>(
file: &'a cst_v1::FileV1,
surfaces: &SurfaceEnv<'a>,
) -> Result<Vec<cst::TopBinding>, LowerError> {
match file {
cst_v1::FileV1::Library {
module_kw,
name,
sig_annot: _,
eq,
struct_kw,
binds,
end_kw,
..
} => {
let module = lower_module_bind(
module_kw,
name,
eq,
struct_kw,
binds,
end_kw,
&[],
&TypeNameEnv::default(),
surfaces,
)?;
Ok(vec![module])
}
cst_v1::FileV1::Document { eoi, .. } => Err(unsupported(
"a document file used as a dependency library",
"the loader's DocumentAsDependency check should have rejected \
this before lowering ever ran",
eoi.0,
)),
}
}
fn lower_module_bind<'a, 's>(
module_kw: &KwModule,
name: &CtorTok,
eq: &DefEqTok,
struct_kw: &KwStruct,
binds: impl IntoIterator<Item = &'a cst_v1::Bind>,
end_kw: &KwEnd,
mod_path: &[String],
tyenv: &TypeNameEnv,
surfaces: &SurfaceEnv<'s>,
) -> Result<cst::TopBinding, LowerError> {
let mut child_path = mod_path.to_vec();
child_path.push(name.name.clone());
let absolutized = functor::rewrite_binds(binds, &AbsolutizeRewrite { surfaces }, &child_path)?;
let child_tyenv = tyenv.child(&child_path, absolutized.iter(), surfaces);
let mut decls = Vec::new();
for b in &absolutized {
for tb in lower_bind_v1(b, &child_path, &child_tyenv, surfaces)? {
decls.push(cst::StructDecl(Box::new(tb)));
}
}
Ok(cst::TopBinding::Module {
kw: module_kw.clone(),
name: name.clone(),
sig: None,
eq: eq.clone(),
struct_kw: struct_kw.clone(),
decls,
end_kw: end_kw.clone(),
})
}
pub fn lower_document_v1(file: &cst_v1::FileV1) -> Result<cst::ast::Expr, LowerError> {
match file {
cst_v1::FileV1::Document { body, .. } => lower_expr(body),
cst_v1::FileV1::Library { end_kw, .. } => Err(unsupported(
"a library file used as the entry document",
"the loader's LibraryAsEntry check should have rejected this \
before lowering ever ran",
end_kw.0,
)),
}
}
fn lower_bind_v1<'s>(
b: &cst_v1::Bind,
mod_path: &[String],
tyenv: &TypeNameEnv,
surfaces: &SurfaceEnv<'s>,
) -> Result<Vec<cst::TopBinding>, LowerError> {
match b {
cst_v1::Bind::Value {
kw,
stage,
name,
params,
eq,
body,
} => {
let (ps, value) = lower_param_units(params, lower_expr(body)?)?;
Ok(vec![cst::TopBinding::Let(cst::TopLet {
let_kw: KwLet(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
name: name.clone(),
ascription: None,
leading_bar: None,
params: ps,
eq: eq.clone(),
value,
})])
}
cst_v1::Bind::ValueInline {
kw,
stage,
ctx,
cmd,
params,
eq,
body,
..
} => Ok(vec![cst::TopBinding::LetInline {
kw: KwLetHorz(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
ctx: ctx.clone(),
cmd: plain_horz(cmd)?,
params: lower_command_params(params)?,
eq: eq.clone(),
value: lower_expr(body)?,
}]),
cst_v1::Bind::ValueBlock {
kw,
stage,
ctx,
cmd,
params,
eq,
body,
..
} => Ok(vec![cst::TopBinding::LetBlock {
kw: KwLetVert(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
ctx: ctx.clone(),
cmd: plain_vert(cmd)?,
params: lower_command_params(params)?,
eq: eq.clone(),
value: lower_expr(body)?,
}]),
cst_v1::Bind::ValueMath {
kw,
stage,
ctx,
cmd,
params,
scripts,
eq,
body,
..
} => Ok(vec![lower_value_math(
kw, stage, ctx, cmd, params, scripts, eq, body,
)?]),
cst_v1::Bind::ValueRec {
kw,
stage,
first,
ands,
..
} => Ok(vec![cst::TopBinding::LetRec {
kw: KwLetRec(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
first: lower_rec_clause(first)?,
ands: ands
.iter()
.map(|a| {
Ok(cst::ast::AndBinding {
and_kw: a.and_kw.clone(),
binding: lower_rec_clause(&a.clause)?,
})
})
.collect::<Result<_, LowerError>>()?,
}]),
cst_v1::Bind::ValueMutable {
kw,
stage,
name,
arrow,
value,
..
} => Ok(vec![cst::TopBinding::LetMutable {
kw: KwLetMutable(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
name: name.clone(),
arrow: arrow.clone(),
value: lower_expr(value)?,
}]),
cst_v1::Bind::Type { kw, first, ands } => {
let mut out = Vec::with_capacity(1 + ands.len());
out.push(lower_type_single(kw, first, tyenv)?);
for a in ands {
out.push(lower_type_single(kw, &a.bind, tyenv)?);
}
Ok(out)
}
cst_v1::Bind::Module {
module_kw,
name,
sig_annot: _,
eq,
body,
} => match &*body.0 {
ast_v1::ModExpr::Struct {
struct_kw,
binds,
end_kw,
} => {
let module = lower_module_bind(
module_kw,
name,
eq,
struct_kw,
binds.iter().map(|b| b.0.as_ref()),
end_kw,
mod_path,
tyenv,
surfaces,
)?;
Ok(vec![module])
}
ast_v1::ModExpr::Var(chain) => Ok(vec![lower_module_alias(
module_kw,
name,
eq,
mod_path,
surfaces,
&chain.render(),
mod_chain_span(chain),
)?]),
ast_v1::ModExpr::App { func, arg: _ } => {
let app_span = mod_chain_span(func);
match surface::frozen_app_target(surfaces, mod_path, app_span) {
Some(Some(surface::AppResolution {
functor_path,
arg_path,
})) => {
let fdef = surfaces
.functors
.get(functor_path)
.expect("a frozen app target always names a registered functor");
let body_binds = functor::functor_body_binds(fdef.body).expect(
"a frozen app target's functor body is always struct-shaped \
(a non-struct body never freezes a resolution)",
);
let arg_segs: Vec<String> =
arg_path.split('.').map(str::to_string).collect();
let substituted =
functor::substitute_binds(body_binds, &fdef.param, &arg_segs)?;
let span = name.span;
let module = lower_module_bind(
module_kw,
name,
eq,
&KwStruct(span),
substituted.iter().map(|sb| sb.0.as_ref()),
&KwEnd(span),
mod_path,
tyenv,
surfaces,
)?;
Ok(vec![module])
}
_ => Err(unsupported(
"a functor application whose functor or argument is unknown",
"an application must name an earlier, concrete module already \
in scope (or an enclosing functor's own parameter, once that \
functor is itself applied)",
mod_chain_span(func),
)),
}
}
ast_v1::ModExpr::Functor { .. } => Ok(Vec::new()),
ast_v1::ModExpr::Coerce { name: target, .. } => Ok(vec![lower_module_alias(
module_kw,
name,
eq,
mod_path,
surfaces,
&target.name,
target.span,
)?]),
},
cst_v1::Bind::Signature { .. } => Ok(Vec::new()),
cst_v1::Bind::Include { kw, body } => match &*body.0 {
ast_v1::ModExpr::Var(chain) => {
match surface::frozen_include_target(surfaces, mod_path, kw.0) {
Some(Some(target_path)) => {
let target_surf = surfaces
.modules
.get(target_path)
.expect("a frozen include target is always a registered module");
let decls = alias_member_decls(kw.0, mod_path, target_path, target_surf)?;
Ok(decls.into_iter().map(|sd| *sd.0).collect())
}
_ => Err(unsupported(
"an `include M` binding naming an unknown module",
"an include must name an earlier module already in scope",
mod_chain_span(chain),
)),
}
}
ast_v1::ModExpr::App { func, arg: _ } => {
let app_span = mod_chain_span(func);
match surface::frozen_app_target(surfaces, mod_path, app_span) {
Some(Some(surface::AppResolution {
functor_path,
arg_path,
})) => {
let fdef = surfaces
.functors
.get(functor_path)
.expect("a frozen app target always names a registered functor");
let body_binds = functor::functor_body_binds(fdef.body).expect(
"a frozen app target's functor body is always struct-shaped \
(a non-struct body never freezes a resolution)",
);
let arg_segs: Vec<String> =
arg_path.split('.').map(str::to_string).collect();
let substituted =
functor::substitute_binds(body_binds, &fdef.param, &arg_segs)?;
let mut out = Vec::new();
for b in substituted.iter().map(|sb| sb.0.as_ref()) {
out.extend(lower_bind_v1(b, mod_path, tyenv, surfaces)?);
}
Ok(out)
}
_ => Err(unsupported(
"an `include` of a functor application whose functor or argument \
is unknown",
"an include must name an earlier, concrete module already in \
scope (or an enclosing functor's own parameter, once that \
functor is itself applied)",
mod_chain_span(func),
)),
}
}
ast_v1::ModExpr::Functor { fun_kw, .. } => Err(unsupported(
"an `include` of a functor literal (`fun (X : S) -> ...`)",
"a functor is not a module value — apply it first \
(`include Make Arg`), or name the application \
(`module M = Make Arg include M`)",
fun_kw.0,
)),
ast_v1::ModExpr::Struct { struct_kw, .. } => Err(unsupported(
"an `include` of an inline `struct … end` literal",
"name the module first: `module N = struct … end include N`",
struct_kw.0,
)),
ast_v1::ModExpr::Coerce { name: target, .. } => Err(unsupported(
"an `include` of a coerced module",
"seal a named module first, then include it",
target.span,
)),
},
}
}
fn mod_chain_span(c: &ast_v1::ModChainV1) -> Span {
match c {
ast_v1::ModChainV1::Long(t) => t.span,
ast_v1::ModChainV1::Single(t) => t.span,
}
}
fn lower_module_alias(
module_kw: &KwModule,
name: &CtorTok,
eq: &DefEqTok,
mod_path: &[String],
surfaces: &SurfaceEnv,
_chain_rendered: &str,
chain_span: Span,
) -> Result<cst::TopBinding, LowerError> {
let span = name.span;
let mut alias_path = mod_path.to_vec();
alias_path.push(name.name.clone());
let target_path = match surface::frozen_alias_target(surfaces, &alias_path) {
Some(Some(t)) => t.clone(),
_ => {
return Err(unsupported(
"a module alias/path binding (`module M = N`) naming an unknown module",
"a module alias must name an earlier module already in scope",
chain_span,
));
}
};
let surface = surfaces
.modules
.get(&target_path)
.expect("a frozen alias target is always a registered module");
let decls = alias_member_decls(span, &alias_path, &target_path, surface)?;
Ok(cst::TopBinding::Module {
kw: module_kw.clone(),
name: name.clone(),
sig: None,
eq: eq.clone(),
struct_kw: KwStruct(span),
decls,
end_kw: KwEnd(span),
})
}
fn alias_member_decls(
span: Span,
alias_path: &[String],
target_path: &str,
surface: &ModSurface,
) -> Result<Vec<cst::StructDecl>, LowerError> {
let mut out = Vec::with_capacity(surface.vals.len() + surface.types.len() + surface.mods.len());
for x in &surface.vals {
let target_ref = apply_chain(
cst::ast::Atomic::VarWithMod(VarWithModTok {
mods: vec![target_path.to_string()],
name: x.clone(),
span,
}),
Vec::new(),
);
out.push(cst::StructDecl(Box::new(cst::TopBinding::Let(
cst::TopLet {
let_kw: KwLet(span),
stage: None,
name: cst::BindName::from(var_tok(x, span)),
ascription: None,
leading_bar: None,
params: Vec::new(),
eq: DefEqTok(span),
value: target_ref,
},
))));
}
for (tname, arity) in &surface.types {
let ctor = var_tok(&format!("{target_path}.{tname}"), span);
let (tyvars, ty) = match *arity {
0 => (
Vec::new(),
cst::ast::TypeExpr::Atom(cst::ast::TypeProd {
first: cst::ast::TypeApp {
head: cst::ast::TypeAtom::Name(ctor),
rest: Vec::new(),
},
rest: Vec::new(),
}),
),
1 => {
let tv = TypeVarTok {
name: "a".to_string(),
span,
};
(
vec![tv.clone()],
cst::ast::TypeExpr::Atom(cst::ast::TypeProd {
first: cst::ast::TypeApp {
head: cst::ast::TypeAtom::Var(tv),
rest: vec![cst::ast::TypeAtom::Name(ctor)],
},
rest: Vec::new(),
}),
)
}
_ => {
return Err(unsupported(
"an alias copy of a type member with arity >= 2",
"the 0.0.6 cst target (`TypeApp`) is single-argument \
(cst.rs:1297-1312) — widen it only when a real \
package needs arity >= 2",
span,
));
}
};
out.push(cst::StructDecl(Box::new(cst::TopBinding::Type(
cst::TypeDecl {
kw: KwType(span),
tyvars,
name: var_tok(&qualify_type_key(alias_path, tname), span),
eq: DefEqTok(span),
body: cst::TypeDeclBody::Synonym(ty),
ands: Vec::new(),
},
))));
}
for (qname, child) in &surface.mods {
let mut child_alias_path = alias_path.to_vec();
child_alias_path.push(qname.clone());
let child_target = format!("{target_path}.{qname}");
let child_decls = alias_member_decls(span, &child_alias_path, &child_target, child)?;
out.push(cst::StructDecl(Box::new(cst::TopBinding::Module {
kw: KwModule(span),
name: CtorTok {
name: qname.clone(),
span,
},
sig: None,
eq: DefEqTok(span),
struct_kw: KwStruct(span),
decls: child_decls,
end_kw: KwEnd(span),
})));
}
Ok(out)
}
fn lower_rec_clause(c: &ast_v1::RecClauseV1) -> Result<cst::ast::RecBinding, LowerError> {
let value_expr = lower_expr(&c.value.0)?;
let (params, value) = if c.params.iter().all(|p| p.opts.is_none()) {
let ps = c
.params
.iter()
.map(|p| lower_param_body(&p.body))
.collect::<Result<_, _>>()?;
(ps, value_expr)
} else {
let (_, chain) = lower_param_units(&c.params, value_expr)?;
(Vec::new(), chain)
};
Ok(cst::ast::RecBinding {
name: c.name.clone(),
ascription: None,
leading_bar: None,
params,
eq: c.eq.clone(),
value: erase_expr(value),
extra: Vec::new(),
})
}
fn lower_type_single(
kw: &KwType,
s: &cst_v1::TypeBindSingleV1,
tyenv: &TypeNameEnv,
) -> Result<cst::TopBinding, LowerError> {
Ok(cst::TopBinding::Type(cst::TypeDecl {
kw: kw.clone(),
tyvars: s.tyvars.clone(),
name: VarTok {
name: tyenv.qualify(&s.name.name),
span: s.name.span,
},
eq: s.eq.clone(),
body: match &s.body {
cst_v1::TypeBodyV1::Variant {
leading_bar,
first,
rest,
} => cst::TypeDeclBody::Variant {
leading_bar: leading_bar.clone(),
first: lower_variant_def(first, tyenv)?,
rest: rest
.iter()
.map(|b| {
Ok(cst::BarVariantDef {
bar: b.bar.clone(),
def: lower_variant_def(&b.def, tyenv)?,
})
})
.collect::<Result<_, LowerError>>()?,
},
cst_v1::TypeBodyV1::Synonym(ty) => {
cst::TypeDeclBody::Synonym(lower_type_expr(ty, tyenv)?)
}
},
ands: Vec::new(),
}))
}
fn lower_variant_def(
v: &cst_v1::VariantDefV1,
tyenv: &TypeNameEnv,
) -> Result<cst::VariantDef, LowerError> {
Ok(cst::VariantDef {
ctor: v.ctor.clone(),
of_ty: v
.of_ty
.as_ref()
.map(|o| {
Ok(cst::OfType {
of_kw: o.of_kw.clone(),
ty: lower_type_expr(&o.ty, tyenv)?,
})
})
.transpose()?,
})
}
pub(crate) fn lower_type_expr(
t: &ast_v1::TypeExpr,
tyenv: &TypeNameEnv,
) -> Result<cst::ast::TypeExpr, LowerError> {
Ok(match t {
ast_v1::TypeExpr::Fun { dom, arrow, cod } => cst::ast::TypeExpr::Fun {
opts: Vec::new(),
dom: lower_type_prod(dom, tyenv)?,
arrow: arrow.clone(),
cod: Box::new(lower_type_expr(cod, tyenv)?),
},
ast_v1::TypeExpr::Atom(p) => cst::ast::TypeExpr::Atom(lower_type_prod(p, tyenv)?),
ast_v1::TypeExpr::OptRowFun {
opt_dom,
dom,
arrow,
cod,
} => {
if let Some(tail) = &opt_dom.inner.row_tail {
return Err(unsupported(
"a row-variable tail in an optional-argument type domain (`| ?'r`)",
"row quantification arrives with signature enforcement — \
roadmap L4 / Sub-slice 2d",
tail.var.span,
));
}
if opt_dom.inner.entries.is_empty() {
return Err(unsupported(
"an empty `?()` optional-argument type domain",
"a `?(…)` domain must bind at least one label",
opt_dom.q.0,
));
}
cst::ast::TypeExpr::OptRowFun {
opt_dom: cst::ast::CstTypeOptDom {
q: opt_dom.q.clone(),
paren: clone_paren(&opt_dom.paren),
entries: opt_dom
.inner
.entries
.iter()
.map(|e| {
Ok(cst::ast::CstTypeOptEntry {
label: e.label.clone(), colon: e.colon.clone(),
ty: cst::TyErased(Box::new(lower_type_expr(&e.ty.0, tyenv)?)),
comma: e.comma.clone(),
})
})
.collect::<Result<_, LowerError>>()?,
},
dom: lower_type_prod(dom, tyenv)?,
arrow: arrow.clone(),
cod: Box::new(lower_type_expr(cod, tyenv)?),
}
}
})
}
fn lower_type_prod(
p: &ast_v1::TypeProd,
tyenv: &TypeNameEnv,
) -> Result<cst::ast::TypeProd, LowerError> {
Ok(cst::ast::TypeProd {
first: lower_type_app(&p.first, tyenv)?,
rest: p
.rest
.iter()
.map(|s| {
Ok(cst::ast::StarType {
star: s.star.clone(),
ty: lower_type_app(&s.ty, tyenv)?,
})
})
.collect::<Result<_, LowerError>>()?,
})
}
fn lower_type_app(
a: &ast_v1::TypeApp,
tyenv: &TypeNameEnv,
) -> Result<cst::ast::TypeApp, LowerError> {
match a {
ast_v1::TypeApp::InlineCmdTy { kw, ilist: list, args } => Ok(cst::ast::TypeApp {
head: cst::ast::TypeAtom::Cmd {
list: list.clone(),
args: lower_type_cmd_args(args, tyenv)?,
kind: cst::ast::CmdTypeKind::Inline(HorzCmdTypeTok(kw.0)),
},
rest: Vec::new(),
}),
ast_v1::TypeApp::BlockCmdTy { kw, blist: list, args } => Ok(cst::ast::TypeApp {
head: cst::ast::TypeAtom::Cmd {
list: list.clone(),
args: lower_type_cmd_args(args, tyenv)?,
kind: cst::ast::CmdTypeKind::Block(VertCmdTypeTok(kw.0)),
},
rest: Vec::new(),
}),
ast_v1::TypeApp::MathCmdTy { kw, mlist: list, args } => Ok(cst::ast::TypeApp {
head: cst::ast::TypeAtom::Cmd {
list: list.clone(),
args: lower_type_cmd_args(args, tyenv)?,
kind: cst::ast::CmdTypeKind::Math(MathCmdTypeTok(kw.0)),
},
rest: Vec::new(),
}),
ast_v1::TypeApp::AppliedLong { ctor, first, rest } => lower_applied(
first,
rest,
qualify_type_key(&ctor.mods, &ctor.name),
ctor.span,
tyenv,
),
ast_v1::TypeApp::Applied { ctor, first, rest } => {
let name = tyenv.qualify(&ctor.name);
lower_applied(first, rest, name, ctor.span, tyenv)
}
ast_v1::TypeApp::Atom(at) => Ok(cst::ast::TypeApp {
head: lower_type_atom(at, tyenv)?,
rest: Vec::new(),
}),
}
}
fn lower_applied(
first: &ast_v1::TypeAtom,
rest: &[ast_v1::TypeAtom],
ctor_name: String,
ctor_span: Span,
tyenv: &TypeNameEnv,
) -> Result<cst::ast::TypeApp, LowerError> {
let head = lower_type_atom(first, tyenv)?;
let mut out_rest: Vec<cst::ast::TypeAtom> = Vec::with_capacity(rest.len() + 1);
for a in rest {
out_rest.push(lower_type_atom(a, tyenv)?);
}
out_rest.push(cst::ast::TypeAtom::Name(VarTok {
name: ctor_name,
span: ctor_span,
}));
Ok(cst::ast::TypeApp {
head,
rest: out_rest,
})
}
fn lower_type_cmd_args(
args: &[ast_v1::TypeCmdArgItemV1],
tyenv: &TypeNameEnv,
) -> Result<Vec<cst::ast::TypeCmdArgItem>, LowerError> {
args.iter()
.map(|a| {
Ok(cst::ast::TypeCmdArgItem {
opt_labels: match &a.opts {
None => Vec::new(),
Some(dom) => {
if dom.entries.is_empty() {
return Err(unsupported(
"an empty `?()` command-type optional-label bundle",
"a `?(…)` bundle must bind at least one label",
dom.q.0,
));
}
dom.entries
.iter()
.map(|e| {
Ok(cst::ast::TypeCmdOptField {
label: e.label.clone(),
colon: e.colon.clone(),
ty: cst::TyErased(Box::new(lower_type_expr(&e.ty.0, tyenv)?)),
comma: e.comma.clone(),
})
})
.collect::<Result<_, LowerError>>()?
}
},
ty: cst::TyErased(Box::new(lower_type_expr(&a.ty.0, tyenv)?)),
opt: None,
semi: None,
})
})
.collect()
}
fn lower_type_atom(
a: &ast_v1::TypeAtom,
tyenv: &TypeNameEnv,
) -> Result<cst::ast::TypeAtom, LowerError> {
Ok(match a {
ast_v1::TypeAtom::Paren { paren, inner } => cst::ast::TypeAtom::Paren {
paren: paren.clone(),
inner: cst::TyErased(Box::new(lower_type_expr(&inner.0, tyenv)?)),
},
ast_v1::TypeAtom::Record { rec, inner } if inner.row_tail.is_none() => {
cst::ast::TypeAtom::Record {
rec: rec.clone(),
fields: inner
.fields
.iter()
.map(|f| {
Ok(cst::ast::TypeRecordField {
name: f.name.clone(), colon: f.colon.clone(), ty: cst::TyErased(Box::new(lower_type_expr(&f.ty.0, tyenv)?)),
semi: None,
})
})
.collect::<Result<_, LowerError>>()?,
}
}
ast_v1::TypeAtom::Record { rec, inner } => {
let tail = inner.row_tail.as_ref().expect("guarded by the arm above");
cst::ast::TypeAtom::RecordOpen {
orec: rec.clone(),
inner: cst::ast::CstRecordOpenInner {
fields: inner
.fields
.iter()
.map(|f| {
Ok(cst::ast::CstRecordOpenField {
name: f.name.clone(),
colon: f.colon.clone(),
ty: cst::TyErased(Box::new(lower_type_expr(&f.ty.0, tyenv)?)),
comma: None,
})
})
.collect::<Result<_, LowerError>>()?,
bar: tail.bar.clone(),
var: tail.var.clone(),
},
}
}
ast_v1::TypeAtom::Var(v) => cst::ast::TypeAtom::Var(v.clone()),
ast_v1::TypeAtom::LongName(t) => cst::ast::TypeAtom::Name(VarTok {
name: qualify_type_key(&t.mods, &t.name),
span: t.span,
}),
ast_v1::TypeAtom::Name(n) => cst::ast::TypeAtom::Name(VarTok {
name: tyenv.qualify(&n.name),
span: n.span,
}),
})
}
fn plain_horz(name: &AnyHorzCmdTok) -> Result<HorzCmdTok, LowerError> {
match name {
AnyHorzCmdTok::Plain(t) => Ok(t.clone()),
AnyHorzCmdTok::Mod(t) => Err(unsupported(
"a module-qualified command name in binding position",
"the cst target field (`LetInline::cmd`) is a bare `HorzCmdTok` \
— not valid 0.1 syntax",
t.span,
)),
}
}
fn plain_vert(name: &AnyVertCmdTok) -> Result<VertCmdTok, LowerError> {
match name {
AnyVertCmdTok::Plain(t) => Ok(t.clone()),
AnyVertCmdTok::Mod(t) => Err(unsupported(
"a module-qualified command name in binding position",
"the cst target field (`LetBlock::cmd`) is a bare `VertCmdTok` \
— not valid 0.1 syntax",
t.span,
)),
}
}
fn lower_param_units(
params: &[cst_v1::Param],
body: cst::ast::Expr,
) -> Result<(Vec<cst::ast::Param>, cst::ast::Expr), LowerError> {
if params.iter().all(|p| p.opts.is_none()) {
let ps = params
.iter()
.map(|p| Ok(cst::ast::Param::Pat(lower_param_body(&p.body)?)))
.collect::<Result<_, LowerError>>()?;
return Ok((ps, body));
}
let mut chain = body;
for p in params.iter().rev() {
let param_pat = lower_param_body(&p.body)?;
chain = match &p.opts {
Some(opts) => cst::ast::Expr::FunRows {
kw: KwFun(opts.q.0),
opts: lower_opt_binders(opts)?,
param: param_pat,
arrow: ArrowTok(opts.q.0),
body: Box::new(chain),
},
None => cst::ast::Expr::Fun {
kw: KwFun(Span::default()),
params: vec![param_pat],
arrow: ArrowTok(Span::default()),
body: Box::new(chain),
},
};
}
Ok((Vec::new(), chain))
}
fn lower_command_params(params: &[cst_v1::Param]) -> Result<Vec<cst::ast::Param>, LowerError> {
params
.iter()
.map(|p| match &p.opts {
None => Ok(cst::ast::Param::Pat(lower_param_body(&p.body)?)),
Some(opts) => Ok(cst::ast::Param::Bundled {
opts: lower_opt_binders(opts)?,
body: lower_param_body(&p.body)?,
}),
})
.collect()
}
fn lower_opt_binders(opts: &ast_v1::OptParamsV1) -> Result<cst::ast::CstOptBinders, LowerError> {
if opts.entries.is_empty() {
return Err(unsupported(
"an empty `?()` optional-parameter bundle",
"a `?(…)` bundle must bind at least one label",
opts.q.0,
));
}
Ok(cst::ast::CstOptBinders {
q: opts.q.clone(),
paren: clone_paren(&opts.paren),
entries: opts
.entries
.iter()
.map(|e| cst::ast::CstOptBinderEntry {
label: e.label.clone(),
eq: e.eq.clone(),
var: e.var.clone(),
comma: e.comma.clone(),
})
.collect(),
})
}
fn lower_opt_args(opts: &ast_v1::OptArgsV1) -> Result<cst::ast::CstOptArgs, LowerError> {
if opts.entries.is_empty() {
return Err(unsupported(
"an empty `?()` optional-argument bundle",
"a `?(…)` bundle must supply at least one label",
opts.q.0,
));
}
Ok(cst::ast::CstOptArgs {
q: opts.q.clone(),
paren: clone_paren(&opts.paren),
entries: opts
.entries
.iter()
.map(|e| {
Ok(cst::ast::CstOptArgEntry {
label: e.label.clone(),
eq: e.eq.clone(),
value: erase_expr(lower_expr(&e.value.0)?),
comma: e.comma.clone(),
})
})
.collect::<Result<_, LowerError>>()?,
})
}
fn clone_paren(p: &ParenGroup<()>) -> ParenGroup<()> {
ParenGroup {
open: p.open.clone(),
slot: (),
close: p.close.clone(),
}
}
fn var_tok(name: &str, span: Span) -> VarTok {
VarTok {
name: name.to_string(),
span,
}
}
fn var_atomic(name: &str, span: Span) -> cst::ast::Atomic {
cst::ast::Atomic::Var(var_tok(name, span))
}
fn paren_atomic(expr: cst::ast::Expr, span: Span) -> cst::ast::Atomic {
cst::ast::Atomic::Paren {
paren: ParenGroup {
open: LParenTok(span),
slot: (),
close: RParenTok(span),
},
inner: Box::new(cst::ast::ParenBody {
first: cst::ExprErased(Box::new(expr)),
rest: Vec::new(),
}),
}
}
fn apply_chain(head: cst::ast::Atomic, args: Vec<cst::ast::Atomic>) -> cst::ast::Expr {
let app_args = args
.into_iter()
.map(|atom| cst::ast::AppArg::Atom {
stage: None,
excl: None,
atom,
accesses: Vec::new(),
})
.collect();
cst::ast::Expr::Ops(cst::ast::OpChain {
head: cst::ast::AppExpr {
minus: None,
stage: None,
excl: None,
head,
head_accesses: Vec::new(),
args: app_args,
},
tail: Vec::new(),
before: None,
})
}
fn fun1(param_name: &str, span: Span, body: cst::ast::Expr) -> cst::ast::Expr {
cst::ast::Expr::Fun {
kw: KwFun(span),
params: vec![cst::ast::PatBot::Var(var_tok(param_name, span))],
arrow: ArrowTok(span),
body: Box::new(body),
}
}
fn lower_value_math(
kw: &KwVal,
stage: &Option<cst_v1::BindStageV1>,
ctx: &VarTok,
cmd: &AnyHorzCmdTok,
params: &[cst_v1::Param],
scripts: &Option<cst_v1::ScriptsParamV1>,
eq: &DefEqTok,
body: &ast_v1::Expr,
) -> Result<cst::TopBinding, LowerError> {
let body = lower_expr(body)?;
let span = eq.0;
let (sub_name, sup_name, wrapped_body) = match scripts {
Some(sp) => (sp.sub.name.clone(), sp.sup.name.clone(), body),
None => (
"%sub".to_string(),
"%sup".to_string(),
apply_chain(
var_atomic("%math-attach-scripts", span),
vec![
var_atomic(&ctx.name, ctx.span),
paren_atomic(body, span),
var_atomic("%sub", span),
var_atomic("%sup", span),
],
),
),
};
let value = fun1(
&ctx.name,
ctx.span,
fun1(&sub_name, span, fun1(&sup_name, span, wrapped_body)),
);
Ok(cst::TopBinding::LetMath {
kw: KwLetMath(kw.0),
stage: stage.as_ref().map(lower_bind_stage),
cmd: plain_horz(cmd)?,
params: lower_command_params(params)?,
eq: eq.clone(),
value,
})
}
fn lower_expr(e: &ast_v1::Expr) -> Result<cst::ast::Expr, LowerError> {
match e {
ast_v1::Expr::LetRecIn {
let_kw,
first,
ands,
in_kw,
body,
..
} => Ok(cst::ast::Expr::LetRecIn {
kw: KwLetRec(let_kw.0),
first: lower_rec_clause(first)?,
ands: ands
.iter()
.map(|a| {
Ok(cst::ast::AndBinding {
and_kw: a.and_kw.clone(),
binding: lower_rec_clause(&a.clause)?,
})
})
.collect::<Result<_, LowerError>>()?,
in_kw: in_kw.clone(),
body: Box::new(lower_expr(body)?),
}),
ast_v1::Expr::LetMutableIn {
let_kw,
name,
arrow,
init,
in_kw,
body,
..
} => Ok(cst::ast::Expr::LetMutableIn {
kw: KwLetMutable(let_kw.0),
name: name.clone(),
arrow: arrow.clone(),
init: Box::new(lower_expr(init)?),
in_kw: in_kw.clone(),
body: Box::new(lower_expr(body)?),
}),
ast_v1::Expr::LetIn {
kw,
name,
params,
eq,
value,
in_kw,
body,
} => {
let (ps, value_expr) = lower_param_units(params, lower_expr(value)?)?;
Ok(cst::ast::Expr::LetIn {
kw: kw.clone(),
name: name.clone(),
ascription: None,
leading_bar: None,
params: ps,
eq: eq.clone(),
value: Box::new(value_expr),
in_kw: in_kw.clone(),
body: Box::new(lower_expr(body)?),
})
}
ast_v1::Expr::LetPatternIn {
kw,
pat,
eq,
value,
in_kw,
body,
} => Ok(cst::ast::Expr::LetPatternIn {
kw: kw.clone(),
pat: erase_pat_non_var(lower_pattern(pat)?),
eq: eq.clone(),
value: Box::new(lower_expr(value)?),
in_kw: in_kw.clone(),
body: Box::new(lower_expr(body)?),
}),
ast_v1::Expr::OpenIn {
open_kw,
name,
in_kw,
body,
..
} => Ok(cst::ast::Expr::OpenIn {
kw: open_kw.clone(),
name: name.clone(),
in_kw: in_kw.clone(),
body: Box::new(lower_expr(body)?),
}),
ast_v1::Expr::If {
kw,
cond,
then_kw,
then_branch,
else_kw,
else_branch,
} => Ok(cst::ast::Expr::If {
kw: kw.clone(),
cond: Box::new(lower_expr(cond)?),
then_kw: then_kw.clone(),
then_branch: Box::new(lower_expr(then_branch)?),
else_kw: else_kw.clone(),
else_branch: Box::new(lower_expr(else_branch)?),
}),
ast_v1::Expr::Fun {
kw,
params,
arrow,
body,
} => {
let body_expr = lower_expr(body)?;
if params.iter().all(|p| p.opts.is_none()) {
Ok(cst::ast::Expr::Fun {
kw: kw.clone(),
params: params
.iter()
.map(|p| lower_param_body(&p.body))
.collect::<Result<_, _>>()?,
arrow: arrow.clone(),
body: Box::new(body_expr),
})
} else {
let (_, chain) = lower_param_units(params, body_expr)?;
Ok(chain)
}
}
ast_v1::Expr::Match {
kw,
scrutinee,
with_kw,
leading_bar,
first,
rest,
..
} => Ok(cst::ast::Expr::Match {
kw: kw.clone(),
scrutinee: Box::new(lower_expr(scrutinee)?),
with_kw: with_kw.clone(),
leading_bar: leading_bar.clone(),
first: lower_match_arm(first)?,
rest: rest.iter().map(lower_bar_arm).collect::<Result<_, _>>()?,
}),
ast_v1::Expr::Overwrite { name, arrow, value } => Ok(cst::ast::Expr::Overwrite {
name: name.clone(),
arrow: arrow.clone(),
value: erase_expr(lower_expr(value)?),
}),
ast_v1::Expr::Ops(chain) => Ok(cst::ast::Expr::Ops(lower_op_chain(chain)?)),
}
}
fn lower_match_arm(a: &ast_v1::MatchArm) -> Result<cst::ast::MatchArm, LowerError> {
Ok(cst::ast::MatchArm {
pat: erase_pat(lower_pattern(&a.pat)?),
guard: None,
arrow: a.arrow.clone(),
body: erase_expr(lower_expr(&a.body)?),
})
}
fn lower_bar_arm(a: &ast_v1::BarArm) -> Result<cst::ast::BarArm, LowerError> {
Ok(cst::ast::BarArm {
bar: a.bar.clone(),
arm: lower_match_arm(&a.arm)?,
})
}
fn lower_op_chain(c: &ast_v1::OpChain) -> Result<cst::ast::OpChain, LowerError> {
Ok(cst::ast::OpChain {
head: lower_app_expr(&c.head)?,
tail: c.tail.iter().map(lower_op_rhs).collect::<Result<_, _>>()?,
before: None,
})
}
fn lower_op_rhs(r: &ast_v1::OpRhs) -> Result<cst::ast::OpRhs, LowerError> {
Ok(cst::ast::OpRhs {
op: r.op.clone(),
rhs: lower_app_expr(&r.rhs)?,
})
}
fn lower_bind_stage(s: &cst_v1::BindStageV1) -> cst::TopStage {
cst::TopStage {
persistent: s.persistent.clone(),
tilde: s.tilde.clone(),
}
}
fn lower_stage_prefix(s: &ast_v1::StagePrefix) -> cst::ast::StagePrefix {
match s {
ast_v1::StagePrefix::Next(t) => cst::ast::StagePrefix::Next(t.clone()),
ast_v1::StagePrefix::Prev(t) => cst::ast::StagePrefix::Prev(t.clone()),
}
}
fn lower_app_expr(e: &ast_v1::AppExpr) -> Result<cst::ast::AppExpr, LowerError> {
Ok(cst::ast::AppExpr {
minus: e.minus.clone(),
stage: e.stage.as_ref().map(lower_stage_prefix),
excl: e.excl.clone(),
head: lower_atomic(&e.head)?,
head_accesses: e.head_accesses.iter().map(lower_access_seg).collect(),
args: e.args.iter().map(lower_app_arg).collect::<Result<_, _>>()?,
})
}
fn lower_access_seg(a: &ast_v1::AccessSeg) -> cst::ast::AccessSeg {
cst::ast::AccessSeg {
hash: a.hash.clone(),
label: a.label.clone(),
}
}
fn lower_app_arg(a: &ast_v1::AppArg) -> Result<cst::ast::AppArg, LowerError> {
match a {
ast_v1::AppArg::Bundled {
opts,
excl,
atom,
accesses,
} => Ok(cst::ast::AppArg::Bundled {
opts: lower_opt_args(opts)?,
excl: excl.clone(),
atom: lower_atomic(atom)?,
accesses: accesses.iter().map(lower_access_seg).collect(),
}),
ast_v1::AppArg::BundledCtor { opts, ctor } => Ok(cst::ast::AppArg::BundledCtor {
opts: lower_opt_args(opts)?,
ctor: ctor.clone(),
}),
ast_v1::AppArg::Atom {
stage,
excl,
atom,
accesses,
} => Ok(cst::ast::AppArg::Atom {
stage: stage.as_ref().map(lower_stage_prefix),
excl: excl.clone(),
atom: lower_atomic(atom)?,
accesses: accesses.iter().map(lower_access_seg).collect(),
}),
ast_v1::AppArg::Ctor(t) => Ok(cst::ast::AppArg::Ctor(t.clone())),
}
}
fn lower_atomic(a: &ast_v1::Atomic) -> Result<cst::ast::Atomic, LowerError> {
match a {
ast_v1::Atomic::Length(t) => Ok(cst::ast::Atomic::Length(t.clone())),
ast_v1::Atomic::Float(t) => Ok(cst::ast::Atomic::Float(t.clone())),
ast_v1::Atomic::Int(t) => Ok(cst::ast::Atomic::Int(t.clone())),
ast_v1::Atomic::Literal(t) => Ok(cst::ast::Atomic::Literal(t.clone())),
ast_v1::Atomic::True(t) => Ok(cst::ast::Atomic::True(t.clone())),
ast_v1::Atomic::False(t) => Ok(cst::ast::Atomic::False(t.clone())),
ast_v1::Atomic::Ctor(t) => Ok(cst::ast::Atomic::Ctor(t.clone())),
ast_v1::Atomic::Var(t) => Ok(cst::ast::Atomic::Var(t.clone())),
ast_v1::Atomic::VarWithMod(t) => Ok(cst::ast::Atomic::VarWithMod(t.clone())),
ast_v1::Atomic::Command { kw, name } => Ok(cst::ast::Atomic::Command {
kw: kw.clone(),
name: name.clone(),
}),
ast_v1::Atomic::Unit { paren } => Ok(cst::ast::Atomic::Unit {
paren: paren.clone(),
}),
ast_v1::Atomic::Paren { paren, inner } => Ok(cst::ast::Atomic::Paren {
paren: paren.clone(),
inner: Box::new(lower_paren_body(inner)?),
}),
ast_v1::Atomic::Record { rec, body } => Ok(cst::ast::Atomic::Record {
rec: rec.clone(),
body: lower_record_body(body)?,
}),
ast_v1::Atomic::List { list, items } => Ok(cst::ast::Atomic::List {
list: list.clone(),
items: items
.iter()
.map(lower_list_item)
.collect::<Result<_, _>>()?,
}),
ast_v1::Atomic::InlineText { igrp, elems } => Ok(cst::ast::Atomic::InlineText {
igrp: igrp.clone(),
elems: elems
.iter()
.map(lower_inline_elem)
.collect::<Result<_, _>>()?,
}),
ast_v1::Atomic::BlockText { bgrp, elems } => Ok(cst::ast::Atomic::BlockText {
bgrp: bgrp.clone(),
elems: elems
.iter()
.map(lower_block_elem)
.collect::<Result<_, _>>()?,
}),
ast_v1::Atomic::MathText { mgrp, elems } => Ok(cst::ast::Atomic::MathText {
mgrp: mgrp.clone(),
elems: lower_math_elems(elems)?,
}),
}
}
fn lower_record_body(b: &ast_v1::RecordBody) -> Result<cst::ast::RecordBody, LowerError> {
match b {
ast_v1::RecordBody::Update {
base,
with_kw,
fields,
} => Ok(cst::ast::RecordBody::Update {
base: erase_expr(lower_expr(base)?),
with_kw: with_kw.clone(),
fields: fields
.iter()
.map(lower_record_field)
.collect::<Result<_, _>>()?,
}),
ast_v1::RecordBody::Fields(fields) => Ok(cst::ast::RecordBody::Fields(
fields
.iter()
.map(lower_record_field)
.collect::<Result<_, _>>()?,
)),
}
}
fn lower_record_field(f: &ast_v1::RecordField) -> Result<cst::ast::RecordField, LowerError> {
Ok(cst::ast::RecordField {
name: f.name.clone(),
eq: f.eq.clone(),
value: erase_expr(lower_expr(&f.value)?),
semi: None,
})
}
fn lower_paren_body(b: &ast_v1::ParenBody) -> Result<cst::ast::ParenBody, LowerError> {
Ok(cst::ast::ParenBody {
first: erase_expr(lower_expr(&b.first)?),
rest: b
.rest
.iter()
.map(lower_comma_expr)
.collect::<Result<_, _>>()?,
})
}
fn lower_comma_expr(c: &ast_v1::CommaExpr) -> Result<cst::ast::CommaExpr, LowerError> {
Ok(cst::ast::CommaExpr {
comma: c.comma.clone(),
value: erase_expr(lower_expr(&c.value)?),
})
}
fn lower_list_item(i: &ast_v1::ListItem) -> Result<cst::ast::ListItem, LowerError> {
Ok(cst::ast::ListItem {
value: erase_expr(lower_expr(&i.value)?),
semi: None,
})
}
fn lower_inline_elem(e: &ast_v1::InlineElem) -> Result<cst::ast::InlineElem, LowerError> {
match e {
ast_v1::InlineElem::Char(t) => Ok(cst::ast::InlineElem::Char(t.clone())),
ast_v1::InlineElem::CodeText(t) => Ok(cst::ast::InlineElem::CodeText(t.clone())),
ast_v1::InlineElem::Space(t) => Ok(cst::ast::InlineElem::Space(t.clone())),
ast_v1::InlineElem::Break(t) => Ok(cst::ast::InlineElem::Break(t.clone())),
ast_v1::InlineElem::Embed { var, semi } => Ok(cst::ast::InlineElem::Embed {
var: var.clone(),
semi: semi.clone(),
}),
ast_v1::InlineElem::EmbedMath { mgrp, elems } => Ok(cst::ast::InlineElem::EmbedMath {
mgrp: mgrp.clone(),
elems: lower_math_elems(elems)?,
}),
ast_v1::InlineElem::Cmd { name, tail } => Ok(cst::ast::InlineElem::Cmd {
name: name.clone(),
tail: lower_cmd_tail(tail)?,
}),
ast_v1::InlineElem::ItemBullet(t) => Ok(cst::ast::InlineElem::ItemBullet(t.clone())),
ast_v1::InlineElem::Sep(t) => Ok(cst::ast::InlineElem::Sep(t.clone())),
}
}
fn lower_block_elem(e: &ast_v1::BlockElem) -> Result<cst::ast::BlockElem, LowerError> {
match e {
ast_v1::BlockElem::Embed { var, semi } => Ok(cst::ast::BlockElem::Embed {
var: var.clone(),
semi: semi.clone(),
}),
ast_v1::BlockElem::Cmd { name, tail } => Ok(cst::ast::BlockElem::Cmd {
name: name.clone(),
tail: lower_cmd_tail(tail)?,
}),
}
}
fn lower_cmd_tail(t: &ast_v1::CmdTail) -> Result<cst::ast::CmdTail, LowerError> {
match t {
ast_v1::CmdTail::Semi(s) => Ok(cst::ast::CmdTail::Semi(s.clone())),
ast_v1::CmdTail::Args {
lead_opts,
args,
semi,
} => {
let ast_v1::Expr::Ops(chain) = &*args.0 else {
return Err(unsupported(
"command arguments that are not a plain application chain",
"grammar-drift guard — the cst_v1 grammar cannot actually \
produce this shape in command-tail position",
Span::default(),
));
};
if !chain.tail.is_empty() || chain.head.minus.is_some() {
return Err(unsupported(
"an operator or unary negation inside a command argument chain",
"grammar-drift guard — the cst_v1 grammar cannot actually \
produce this shape in command-tail position",
Span::default(),
));
}
let a = &chain.head;
let first = cst::AppArgErased(Box::new(match lead_opts {
Some(opts) => {
if a.stage.is_some() {
return Err(unsupported(
"a staging prefix on a command argument that also \
carries a `?(l = e, …)` bundle",
"the lowered 0.0.6 node has no stage slot on a \
bundled argument; write the `&`/`~` inside the \
parenthesized argument instead",
Span::default(),
));
}
cst::ast::AppArg::Bundled {
opts: lower_opt_args(opts)?,
excl: a.excl.clone(),
atom: lower_atomic(&a.head)?,
accesses: a.head_accesses.iter().map(lower_access_seg).collect(),
}
}
None => cst::ast::AppArg::Atom {
stage: a.stage.as_ref().map(lower_stage_prefix),
excl: a.excl.clone(),
atom: lower_atomic(&a.head)?,
accesses: a.head_accesses.iter().map(lower_access_seg).collect(),
},
}));
let rest = a
.args
.iter()
.map(|arg| Ok(cst::AppArgErased(Box::new(lower_app_arg(arg)?))))
.collect::<Result<Vec<_>, LowerError>>()?;
Ok(cst::ast::CmdTail::Args {
first,
rest,
semi: semi.clone(),
})
}
}
}
fn lower_math_elems(elems: &[cst_v1::MathErasedV1]) -> Result<Vec<cst::MathErased>, LowerError> {
elems
.iter()
.map(|e| Ok(cst::MathErased(Box::new(lower_math_elem_cst(e)?))))
.collect()
}
fn lower_math_elem_cst(m: &ast_v1::MathElemCst) -> Result<cst::ast::MathElemCst, LowerError> {
Ok(cst::ast::MathElemCst {
base: lower_math_bot(&m.base)?,
scripts: m
.scripts
.iter()
.map(lower_math_script)
.collect::<Result<_, _>>()?,
})
}
fn lower_math_bot(b: &ast_v1::MathBot) -> Result<cst::ast::MathBot, LowerError> {
Ok(match b {
ast_v1::MathBot::Cmd { name, args } => cst::ast::MathBot::Cmd {
name: name.clone(),
args: args.iter().map(lower_math_arg).collect::<Result<_, _>>()?,
},
ast_v1::MathBot::Chars(t) => cst::ast::MathBot::Chars(t.clone()),
ast_v1::MathBot::Embed(t) => cst::ast::MathBot::Embed(t.clone()),
ast_v1::MathBot::Sep(t) => cst::ast::MathBot::Sep(t.clone()),
ast_v1::MathBot::Group { mgrp, elems } => cst::ast::MathBot::Group {
mgrp: mgrp.clone(),
elems: lower_math_elems(elems)?,
},
})
}
fn lower_math_script(s: &ast_v1::MathScript) -> Result<cst::ast::MathScript, LowerError> {
Ok(match s {
ast_v1::MathScript::Super { hat, group } => cst::ast::MathScript::Super {
hat: hat.clone(),
group: lower_math_group_arg(group)?,
},
ast_v1::MathScript::Sub { under, group } => cst::ast::MathScript::Sub {
under: under.clone(),
group: lower_math_group_arg(group)?,
},
ast_v1::MathScript::Primes(t) => cst::ast::MathScript::Primes(t.clone()),
})
}
fn lower_math_group_arg(g: &ast_v1::MathGroupArg) -> Result<cst::ast::MathGroupArg, LowerError> {
Ok(match g {
ast_v1::MathGroupArg::Group { mgrp, elems } => cst::ast::MathGroupArg::Group {
mgrp: mgrp.clone(),
elems: lower_math_elems(elems)?,
},
ast_v1::MathGroupArg::Bot(b) => cst::ast::MathGroupArg::Bot(Box::new(lower_math_bot(b)?)),
})
}
fn lower_math_arg(a: &ast_v1::MathArg) -> Result<cst::ast::MathArg, LowerError> {
Ok(cst::ast::MathArg::Plain(match a {
ast_v1::MathArg::Math { mgrp, elems } => cst::ast::MathArgBody::Math {
mgrp: mgrp.clone(),
elems: lower_math_elems(elems)?,
},
ast_v1::MathArg::Inline { igrp, elems } => cst::ast::MathArgBody::Inline {
igrp: igrp.clone(),
elems: elems
.iter()
.map(lower_inline_elem)
.collect::<Result<_, _>>()?,
},
ast_v1::MathArg::Block { bgrp, elems } => cst::ast::MathArgBody::Block {
bgrp: bgrp.clone(),
elems: elems
.iter()
.map(lower_block_elem)
.collect::<Result<_, _>>()?,
},
ast_v1::MathArg::ParenEscape { paren, inner } => cst::ast::MathArgBody::ParenEscape {
paren: paren.clone(),
inner: Box::new(lower_paren_body(inner)?),
},
ast_v1::MathArg::ListEscape { list, items } => cst::ast::MathArgBody::ListEscape {
list: list.clone(),
items: items
.iter()
.map(lower_list_item)
.collect::<Result<_, _>>()?,
},
ast_v1::MathArg::RecordEscape { rec, body } => cst::ast::MathArgBody::RecordEscape {
rec: rec.clone(),
body: lower_record_body(body)?,
},
}))
}
fn lower_pattern(p: &ast_v1::Pattern) -> Result<cst::ast::Pattern, LowerError> {
Ok(cst::ast::Pattern {
head: lower_pat_cons(&p.head)?,
as_clause: p.as_clause.as_ref().map(lower_as_clause),
})
}
fn lower_as_clause(a: &ast_v1::AsClause) -> cst::ast::AsClause {
cst::ast::AsClause {
as_kw: a.as_kw.clone(),
name: a.name.clone(),
}
}
fn lower_pat_cons(c: &ast_v1::PatCons) -> Result<cst::ast::PatCons, LowerError> {
Ok(cst::ast::PatCons {
head: lower_pat_bot(&c.head)?,
tail: c
.tail
.iter()
.map(lower_cons_seg)
.collect::<Result<_, _>>()?,
})
}
fn lower_cons_seg(s: &ast_v1::ConsSeg) -> Result<cst::ast::ConsSeg, LowerError> {
Ok(cst::ast::ConsSeg {
cons: s.cons.clone(),
tail: lower_pat_bot(&s.tail)?,
})
}
fn lower_pat_bot(p: &ast_v1::PatBot) -> Result<cst::ast::PatBot, LowerError> {
match p {
ast_v1::PatBot::CtorApplied { ctor, arg } => Ok(cst::ast::PatBot::CtorApplied {
ctor: ctor.clone(),
arg: Box::new(lower_pat_bot(arg)?),
}),
ast_v1::PatBot::Ctor(t) => Ok(cst::ast::PatBot::Ctor(t.clone())),
ast_v1::PatBot::Int(t) => Ok(cst::ast::PatBot::Int(t.clone())),
ast_v1::PatBot::True(t) => Ok(cst::ast::PatBot::True(t.clone())),
ast_v1::PatBot::False(t) => Ok(cst::ast::PatBot::False(t.clone())),
ast_v1::PatBot::Str(t) => Ok(cst::ast::PatBot::Str(t.clone())),
ast_v1::PatBot::Wild(t) => Ok(cst::ast::PatBot::Wild(t.clone())),
ast_v1::PatBot::Var(t) => Ok(cst::ast::PatBot::Var(t.clone())),
ast_v1::PatBot::Unit { paren } => Ok(cst::ast::PatBot::Unit {
paren: paren.clone(),
}),
ast_v1::PatBot::Paren { paren, inner } => Ok(cst::ast::PatBot::Paren {
paren: paren.clone(),
inner: Box::new(lower_pattern_paren_body(inner)?),
}),
ast_v1::PatBot::List { plist, items } => Ok(cst::ast::PatBot::List {
plist: plist.clone(),
items: items
.iter()
.map(lower_pat_list_item)
.collect::<Result<_, _>>()?,
}),
}
}
fn lower_param_body(pb: &ast_v1::ParamBody) -> Result<cst::ast::PatBot, LowerError> {
match pb {
ast_v1::ParamBody::Pat(p) => lower_pat_bot(p),
ast_v1::ParamBody::Ascribed { paren, inner } => Ok(cst::ast::PatBot::Paren {
paren: paren.clone(),
inner: Box::new(cst::ast::PatternParenBody {
first: erase_pat(lower_pattern(&inner.pat)?),
rest: Vec::new(),
}),
}),
}
}
fn lower_pattern_paren_body(
b: &ast_v1::PatternParenBody,
) -> Result<cst::ast::PatternParenBody, LowerError> {
Ok(cst::ast::PatternParenBody {
first: erase_pat(lower_pattern(&b.first)?),
rest: b
.rest
.iter()
.map(lower_comma_pattern)
.collect::<Result<_, _>>()?,
})
}
fn lower_comma_pattern(c: &ast_v1::CommaPattern) -> Result<cst::ast::CommaPattern, LowerError> {
Ok(cst::ast::CommaPattern {
comma: c.comma.clone(),
value: erase_pat(lower_pattern(&c.value)?),
})
}
fn lower_pat_list_item(i: &ast_v1::PatListItem) -> Result<cst::ast::PatListItem, LowerError> {
Ok(cst::ast::PatListItem {
value: erase_pat(lower_pattern(&i.value)?),
semi: None,
})
}
fn erase_expr(e: cst::ast::Expr) -> cst::ExprErased {
cst::ExprErased(Box::new(e))
}
fn erase_pat(p: cst::ast::Pattern) -> cst::PatErased {
cst::PatErased(Box::new(p))
}
fn erase_pat_non_var(p: cst::ast::Pattern) -> cst::PatNonVarErased {
debug_assert!(
!p.is_bare_var(),
"a destructuring `let`'s target lowered to a bare variable"
);
cst::PatNonVarErased(Box::new(p))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_v1(src: &str) -> cst_v1::FileV1 {
rustyfi_syntax::parse_file_v1(src).unwrap_or_else(|e| panic!("v1 parse failed: {e}"))
}
#[test]
fn let_rec_document_lowers_with_and_chain() {
let file = parse_v1(
"let rec even n = if n <= 0 then true else odd (n - 1)\n\
and odd n = if n <= 0 then false else even (n - 1) in even 4",
);
let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
let cst::ast::Expr::LetRecIn { first, ands, .. } = ast else {
panic!("expected Expr::LetRecIn");
};
assert_eq!(ands.len(), 1, "one `and` continuation");
assert!(first.ascription.is_none());
assert!(first.leading_bar.is_none());
assert!(first.extra.is_empty());
assert_eq!(first.name.name, "even");
assert_eq!(ands[0].binding.name.name, "odd");
}
#[test]
fn val_rec_library_lowers_to_top_binding_letrec() {
let file = parse_v1(
"module M = struct\n\
val rec even n = odd n\n\
and odd n = even n\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 1);
let cst::TopBinding::LetRec { first, ands, .. } = &*decls[0].0 else {
panic!("expected TopBinding::LetRec, got {:?}", decls[0].0);
};
assert_eq!(first.name.name, "even");
assert_eq!(ands.len(), 1);
assert_eq!(ands[0].binding.name.name, "odd");
}
#[test]
fn val_mutable_lowers_to_top_binding_letmutable() {
let file = parse_v1("module M = struct\nval mutable c <- 0\nend");
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 1);
assert!(
matches!(&*decls[0].0, cst::TopBinding::LetMutable { name, .. } if name.name == "c"),
"{:?}",
decls[0].0
);
}
#[test]
fn let_mutable_in_document_lowers_to_expr_letmutablein() {
let file = parse_v1("let mutable c <- 0 in c <- !c + 1");
let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
assert!(
matches!(&ast, cst::ast::Expr::LetMutableIn { name, .. } if name.name == "c"),
"{ast:?}"
);
}
#[test]
fn type_and_chain_inside_module_qualifies_names_and_synonym_reference() {
let file = parse_v1(
"module M = struct\n\
type t = int\n\
and u = t\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(
decls.len(),
2,
"an `and`-chain lowers to N consecutive Type decls"
);
let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
panic!("expected decls[0] to be a Type decl");
};
assert_eq!(t_decl.name.name, "M.t");
let cst::TopBinding::Type(u_decl) = &*decls[1].0 else {
panic!("expected decls[1] to be a Type decl");
};
assert_eq!(u_decl.name.name, "M.u");
let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
panic!("expected a synonym body");
};
let cst::ast::TypeExpr::Atom(prod) = ty else {
panic!("expected a bare TypeProd (no arrow)");
};
let cst::ast::TypeApp {
head: cst::ast::TypeAtom::Name(n),
..
} = &prod.first
else {
panic!("expected a bare type name atom");
};
assert_eq!(
n.name, "M.t",
"u's synonym body must reference the QUALIFIED t"
);
}
#[test]
fn nested_module_type_reference_qualifies_to_outer_path() {
let file = parse_v1(
"module M = struct\n\
type t = int\n\
module N = struct\n\
type u = t\n\
end\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 2);
let cst::TopBinding::Module {
name: inner_name,
decls: inner_decls,
..
} = &*decls[1].0
else {
panic!("expected decls[1] to be a nested TopBinding::Module");
};
assert_eq!(inner_name.name, "N");
assert_eq!(inner_decls.len(), 1);
let cst::TopBinding::Type(u_decl) = &*inner_decls[0].0 else {
panic!("expected a Type decl");
};
assert_eq!(u_decl.name.name, "M.N.u");
let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
panic!("expected a synonym body");
};
let cst::ast::TypeExpr::Atom(prod) = ty else {
panic!("expected a bare TypeProd");
};
let cst::ast::TypeApp {
head: cst::ast::TypeAtom::Name(n),
..
} = &prod.first
else {
panic!("expected a bare type name atom");
};
assert_eq!(
n.name, "M.t",
"the outer M.t must stay visible/qualified inside N"
);
}
#[test]
fn type_app_prefix_to_postfix_bridge() {
let file = parse_v1("module M = struct\ntype t = option int\nend");
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
panic!("expected a Type decl");
};
let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
panic!("expected a synonym body");
};
let cst::ast::TypeExpr::Atom(prod) = ty else {
panic!("expected a bare TypeProd");
};
assert!(prod.first.rest.len() == 1, "{:?}", prod.first);
assert!(
matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "option"),
"{:?}",
prod.first.rest[0]
);
assert!(
matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
"{:?}",
prod.first.head
);
}
#[test]
fn type_app_arity_2_lowers_to_nary_atom_run() {
let file = parse_v1("module M = struct\ntype t = pair int int\nend");
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
panic!("expected a Type decl");
};
let cst::TypeDeclBody::Synonym(cst::ast::TypeExpr::Atom(prod)) = &t_decl.body else {
panic!("expected a bare synonym TypeProd");
};
assert!(
matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
"{:?}",
prod.first.head
);
assert_eq!(prod.first.rest.len(), 2, "{:?}", prod.first.rest);
assert!(matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "int"));
assert!(matches!(&prod.first.rest[1], cst::ast::TypeAtom::Name(n) if n.name == "pair"));
}
#[test]
fn mutual_variant_pair_lowers_to_two_type_decls() {
let file = parse_v1(
"module M = struct\n\
type a = A of b\n\
and b = B of a\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 2);
assert!(matches!(&*decls[0].0, cst::TopBinding::Type(d) if d.name.name == "M.a"));
assert!(matches!(&*decls[1].0, cst::TopBinding::Type(d) if d.name.name == "M.b"));
}
#[test]
fn type_record_lowers_to_cst_record_atom() {
let file = parse_v1("module M = struct\ntype t = (| x : int, y : bool |)\nend");
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
panic!("expected a Type decl");
};
let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
panic!("expected a synonym body");
};
let cst::ast::TypeExpr::Atom(prod) = ty else {
panic!("expected a bare TypeProd");
};
let cst::ast::TypeApp {
head: cst::ast::TypeAtom::Record { fields, .. },
..
} = &prod.first
else {
panic!("expected TypeAtom::Record, got {:?}", prod.first);
};
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name.name, "x");
assert!(
matches!(&*fields[0].ty.0, cst::ast::TypeExpr::Atom(p)
if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "int")),
"{:?}",
fields[0].ty.0
);
assert_eq!(fields[1].name.name, "y");
assert!(
matches!(&*fields[1].ty.0, cst::ast::TypeExpr::Atom(p)
if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "bool")),
"{:?}",
fields[1].ty.0
);
}
#[test]
fn type_record_field_type_is_qualified_like_any_other_type_position() {
let file = parse_v1(
"module M = struct\n\
type config = int\n\
type t = (| c : config |)\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 2);
let cst::TopBinding::Type(t_decl) = &*decls[1].0 else {
panic!("expected decls[1] to be a Type decl");
};
assert_eq!(t_decl.name.name, "M.t");
let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
panic!("expected a synonym body");
};
let cst::ast::TypeExpr::Atom(prod) = ty else {
panic!("expected a bare TypeProd");
};
let cst::ast::TypeApp {
head: cst::ast::TypeAtom::Record { fields, .. },
..
} = &prod.first
else {
panic!("expected TypeAtom::Record, got {:?}", prod.first);
};
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name.name, "c");
let cst::ast::TypeExpr::Atom(field_prod) = &*fields[0].ty.0 else {
panic!("expected a bare TypeProd for the field type");
};
let cst::ast::TypeApp {
head: cst::ast::TypeAtom::Name(n),
..
} = &field_prod.first
else {
panic!("expected a bare type name atom, got {:?}", field_prod.first);
};
assert_eq!(
n.name, "M.config",
"the field's bare `config` must qualify to M.config"
);
}
#[test]
fn old_optional_sigils_no_longer_parse() {
assert!(rustyfi_syntax::parse_file_v1("f ?:1").is_err());
assert!(rustyfi_syntax::parse_file_v1("f ?*").is_err());
}
#[test]
fn empty_opt_arg_bundle_is_a_lower_error() {
let file = parse_v1("f ?() x");
let err = lower_document_v1(&file).unwrap_err();
assert!(
err.to_string().contains("optional-argument bundle"),
"{err}"
);
}
#[test]
fn math_text_lowers_structurally() {
let file = parse_v1("${x}");
let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
let cst::ast::Expr::Ops(chain) = &ast else {
panic!("expected Expr::Ops, got {ast:?}");
};
let cst::ast::Atomic::MathText { elems, .. } = &chain.head.head else {
panic!("expected Atomic::MathText, got {:?}", chain.head.head);
};
assert_eq!(elems.len(), 1, "one math element (`x`)");
let cst::ast::MathBot::Chars(t) = &elems[0].base else {
panic!("expected MathBot::Chars, got {:?}", elems[0].base);
};
assert_eq!(t.text, "x");
}
#[test]
fn mod_qualified_command_name_in_bind_is_a_lower_error() {
let tok = HorzCmdWithModTok {
mods: vec!["Mod".to_string()],
name: "\\emph".to_string(),
span: Span::default(),
};
let err = plain_horz(&AnyHorzCmdTok::Mod(tok)).unwrap_err();
assert!(err.to_string().contains("module-qualified"), "{err}");
let tok = VertCmdWithModTok {
mods: vec!["Mod".to_string()],
name: "+p".to_string(),
span: Span::default(),
};
let err = plain_vert(&AnyVertCmdTok::Mod(tok)).unwrap_err();
assert!(err.to_string().contains("module-qualified"), "{err}");
}
#[test]
fn lower_file_v1_on_a_document_is_an_error_not_a_panic() {
let file = parse_v1("3");
assert!(lower_file_v1(&file).is_err());
}
#[test]
fn lower_document_v1_on_a_library_is_an_error_not_a_panic() {
let file = parse_v1("module M = struct\nval x = 1\nend");
assert!(lower_document_v1(&file).is_err());
}
#[test]
fn lower_file_v1_yields_one_real_module_binding() {
let file = parse_v1(
"module V01Mini = struct\n\
val x = 1\n\
val y = 2\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
assert_eq!(
lowered.len(),
1,
"one TopBinding::Module, not spliced binds"
);
let cst::TopBinding::Module {
name, sig, decls, ..
} = &lowered[0]
else {
panic!("expected a TopBinding::Module, got {:?}", lowered[0]);
};
assert_eq!(name.name, "V01Mini");
assert!(sig.is_none(), "no signature annotation in Sub-slice 2a");
assert_eq!(decls.len(), 2);
}
#[test]
fn lower_file_v1_nested_module_bind_lowers_to_nested_module() {
let file = parse_v1(
"module M = struct\n\
val x = 1\n\
module N = struct\n\
val y = 2\n\
end\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
assert_eq!(lowered.len(), 1);
let cst::TopBinding::Module { name, decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(name.name, "M");
assert_eq!(decls.len(), 2);
assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
let cst::TopBinding::Module {
name: inner_name,
sig: inner_sig,
decls: inner_decls,
..
} = &*decls[1].0
else {
panic!("expected decls[1] to be a nested TopBinding::Module");
};
assert_eq!(inner_name.name, "N");
assert!(inner_sig.is_none());
assert_eq!(inner_decls.len(), 1);
}
#[test]
fn sig_annot_on_library_lowers_like_its_unsealed_twin() {
let sealed = parse_v1("module M :> sig val x : int end = struct\nval x = 1\nend");
let unsealed = parse_v1("module M = struct\nval x = 1\nend");
let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
assert_eq!(sealed_lowered.len(), 1);
assert_eq!(unsealed_lowered.len(), 1);
let cst::TopBinding::Module {
sig: sealed_sig,
decls: sealed_decls,
..
} = &sealed_lowered[0]
else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Module {
sig: unsealed_sig,
decls: unsealed_decls,
..
} = &unsealed_lowered[0]
else {
panic!("expected a TopBinding::Module");
};
assert!(
sealed_sig.is_none(),
"the seal must lower to NO cst::SigAnnot at all"
);
assert!(unsealed_sig.is_none());
assert_eq!(sealed_decls.len(), unsealed_decls.len());
assert!(matches!(&*sealed_decls[0].0, cst::TopBinding::Let(_)));
assert!(matches!(&*unsealed_decls[0].0, cst::TopBinding::Let(_)));
}
#[test]
fn sig_annot_body_error_is_identical_with_or_without_a_seal() {
let sealed = parse_v1("module M :> sig end = struct\ninclude struct val x = 1 end\nend");
let unsealed = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
let sealed_err = lower_file_v1(&sealed).unwrap_err();
let unsealed_err = lower_file_v1(&unsealed).unwrap_err();
assert!(
sealed_err.to_string().contains("inline `struct"),
"{sealed_err}"
);
assert_eq!(sealed_err.construct, unsealed_err.construct);
assert_eq!(sealed_err.hint, unsealed_err.hint);
}
#[test]
fn nested_module_sig_annot_lowers_like_its_unsealed_twin() {
let sealed = parse_v1(
"module M = struct\n\
module N :> sig val y : int end = struct\n\
val y = 2\n\
end\n\
end",
);
let unsealed = parse_v1(
"module M = struct\n\
module N = struct\n\
val y = 2\n\
end\n\
end",
);
let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
let cst::TopBinding::Module {
decls: sealed_decls,
..
} = &sealed_lowered[0]
else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Module {
decls: unsealed_decls,
..
} = &unsealed_lowered[0]
else {
panic!("expected a TopBinding::Module");
};
assert_eq!(sealed_decls.len(), 1);
assert_eq!(unsealed_decls.len(), 1);
let cst::TopBinding::Module {
name: sealed_name,
sig: sealed_sig,
decls: sealed_inner,
..
} = &*sealed_decls[0].0
else {
panic!("expected decls[0] to be a nested TopBinding::Module");
};
let cst::TopBinding::Module {
name: unsealed_name,
sig: unsealed_sig,
decls: unsealed_inner,
..
} = &*unsealed_decls[0].0
else {
panic!("expected decls[0] to be a nested TopBinding::Module");
};
assert_eq!(sealed_name.name, "N");
assert_eq!(unsealed_name.name, "N");
assert!(
sealed_sig.is_none(),
"the seal must lower to NO cst::SigAnnot at all"
);
assert!(unsealed_sig.is_none());
assert_eq!(sealed_inner.len(), unsealed_inner.len());
}
#[test]
fn module_alias_with_unknown_target_is_a_lower_error() {
let file = parse_v1("module M = struct\nmodule P = N\nend");
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("module alias"), "{err}");
}
#[test]
fn module_alias_to_a_real_target_lowers_member_copies() {
let file = parse_v1(
"module M = struct\n\
module Base = struct val x = 1 val f y = y end\n\
module Alias = Base\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Module {
name,
decls: alias_decls,
..
} = &*decls[1].0
else {
panic!("expected decls[1] to be the Alias module");
};
assert_eq!(name.name, "Alias");
assert_eq!(alias_decls.len(), 2, "one copy per exported value member");
for (decl, expected_name) in alias_decls.iter().zip(["x", "f"]) {
let cst::TopBinding::Let(top_let) = &*decl.0 else {
panic!("expected a TopBinding::Let copy");
};
assert_eq!(top_let.name.name, expected_name);
let cst::ast::Expr::Ops(chain) = &top_let.value else {
panic!("expected an application-chain expression");
};
let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
panic!("expected a VarWithMod reference to the target");
};
assert_eq!(tok.mods, vec!["M.Base".to_string()]);
assert_eq!(tok.name, expected_name);
}
}
#[test]
fn module_alias_forward_reference_is_a_lower_error() {
let file = parse_v1(
"module M = struct\n\
module Early = Later\n\
module Later = struct val x = 1 end\n\
end",
);
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("module alias"), "{err}");
}
#[test]
fn functor_application_is_a_lower_error() {
let file = parse_v1("module M = struct\nmodule P = F X\nend");
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("functor application"), "{err}");
}
#[test]
fn functor_literal_emits_zero_runtime_bindings() {
let file = parse_v1(
"module M = struct\n\
module F = fun (X : sig val x : int end) -> struct val y = X.x end\n\
end",
);
let bindings =
lower_file_v1(&file).expect("a functor definition now lowers, emitting no member");
let cst::TopBinding::Module { decls, .. } = &bindings[0] else {
panic!("expected M's TopBinding::Module")
};
assert!(
decls.is_empty(),
"a functor literal contributes no decls: {decls:?}"
);
}
#[test]
fn module_coercion_with_unknown_target_is_a_lower_error() {
let file = parse_v1("module M = struct\nmodule P = N :> S\nend");
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("module alias"), "{err}");
}
#[test]
fn module_coercion_lowers_the_same_copies_as_its_uncoerced_twin() {
fn alias_copy_names(src: &str) -> (Vec<String>, Vec<Vec<String>>) {
let lowered = lower_file_v1(&parse_v1(src)).unwrap_or_else(|e| panic!("{e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
let cst::TopBinding::Module {
name,
decls: alias_decls,
..
} = &*decls[1].0
else {
panic!("expected decls[1] to be the Alias module");
};
assert_eq!(name.name, "Alias");
let mut names = Vec::new();
let mut refs = Vec::new();
for d in alias_decls {
let cst::TopBinding::Let(top_let) = &*d.0 else {
panic!("expected a Let copy");
};
names.push(top_let.name.name.clone());
let cst::ast::Expr::Ops(chain) = &top_let.value else {
panic!("expected an application chain");
};
let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
panic!("expected a VarWithMod reference");
};
refs.push(tok.mods.clone());
}
(names, refs)
}
let bare = alias_copy_names(
"module M = struct\n\
module Base = struct val x = 1 end\n\
module Alias = Base\n\
end",
);
let coerced = alias_copy_names(
"module M = struct\n\
module Base = struct val x = 1 end\n\
module Alias = Base :> sig val x : int end\n\
end",
);
assert_eq!(bare, coerced);
assert_eq!(bare.0, vec!["x".to_string()]);
assert_eq!(bare.1, vec![vec!["M.Base".to_string()]]);
}
#[test]
fn signature_bind_lowers_to_nothing() {
let file = parse_v1("module M = struct\nsignature S = sig end\nval x = 1\nend");
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 1, "the signature bind contributes zero decls");
assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
}
#[test]
fn include_of_an_unknown_module_is_a_lower_error() {
let file = parse_v1("module M = struct\ninclude N\nend");
let err = lower_file_v1(&file).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("include"), "{msg}");
assert!(msg.contains("unknown module"), "{msg}");
}
#[test]
fn include_of_a_real_target_splices_member_copies_unwrapped() {
let file = parse_v1(
"module M = struct\n\
module Base = struct val x = 1 val f y = y end\n\
include Base\n\
end",
);
let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
panic!("expected a TopBinding::Module");
};
assert_eq!(decls.len(), 3, "Base + 2 spliced copies, unwrapped");
for (decl, expected_name) in decls[1..].iter().zip(["x", "f"]) {
let cst::TopBinding::Let(top_let) = &*decl.0 else {
panic!("expected a TopBinding::Let copy, got {:?}", decl.0);
};
assert_eq!(top_let.name.name, expected_name);
let cst::ast::Expr::Ops(chain) = &top_let.value else {
panic!("expected an application-chain expression");
};
let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
panic!("expected a VarWithMod reference to the target");
};
assert_eq!(tok.mods, vec!["M.Base".to_string()]);
assert_eq!(tok.name, expected_name);
}
}
#[test]
fn include_forward_reference_is_a_lower_error() {
let file = parse_v1(
"module M = struct\n\
include Later\n\
module Later = struct val x = 1 end\n\
end",
);
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("unknown module"), "{err}");
}
#[test]
fn include_of_a_functor_application_is_the_2f_functor_error() {
let file = parse_v1("module M = struct\nmodule F = struct end\ninclude F X\nend");
let err = lower_file_v1(&file).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("functor application"), "{msg}");
assert!(msg.contains("unknown"), "{msg}");
}
#[test]
fn include_of_an_inline_struct_literal_is_a_lower_error() {
let file = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
let err = lower_file_v1(&file).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("struct"), "{msg}");
assert!(msg.contains("name the module first"), "{msg}");
}
#[test]
fn include_of_a_coerced_module_is_a_lower_error() {
let file = parse_v1(
"module M = struct\n\
module Base = struct val x = 1 end\n\
include Base :> sig val x : int end\n\
end",
);
let err = lower_file_v1(&file).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("coerced module"), "{msg}");
}
#[test]
fn self_include_is_a_lower_error() {
let file = parse_v1("module P = struct\ninclude P\nend");
let err = lower_file_v1(&file).unwrap_err();
assert!(err.to_string().contains("unknown module"), "{err}");
}
#[test]
fn cmd_tail_bridge_matches_flat_app_arg_shape() {
let file = parse_v1(r"{\cmd{a}{b}}");
let cst_v1::FileV1::Document { body, .. } = file else {
panic!("expected a document file");
};
let ast_v1::Expr::Ops(chain) = body else {
panic!("expected an operator-chain expression");
};
let ast_v1::Atomic::InlineText { elems, .. } = chain.head.head else {
panic!("expected inline text");
};
let ast_v1::InlineElem::Cmd { tail, .. } = &elems[0] else {
panic!("expected the first element to be a command");
};
let lowered = lower_cmd_tail(tail).unwrap();
let cst::ast::CmdTail::Args { first, rest, .. } = lowered else {
panic!("expected CmdTail::Args");
};
assert_eq!(
rest.len(),
1,
"\\cmd{{a}}{{b}} has exactly one trailing arg"
);
assert!(matches!(
&*first.0,
cst::ast::AppArg::Atom {
stage: None,
atom: cst::ast::Atomic::InlineText { .. },
..
}
));
assert!(matches!(
&*rest[0].0,
cst::ast::AppArg::Atom {
stage: None,
atom: cst::ast::Atomic::InlineText { .. },
..
}
));
}
}