use crate::ast::branded::{Ast, BText, CmdArg, IText, MatchArm, MathElem, Pattern};
use crate::symbol::{Symbol, SymbolStore};
use rustyfi_backend::Length;
use rustyfi_syntax::cst::{self, ast as c};
use rustyfi_syntax::leaf::{AnyHorzCmdTok, AnyMathCmdTok, AnyVertCmdTok, UnopExclamTok, VarTok};
use rustyfi_syntax::span::Span;
use rustyfi_syntax::token::Token;
use rustyfi_syntax::RustyfiVersion;
use std::collections::{HashMap, HashSet, VecDeque};
use std::rc::Rc;
#[derive(Debug, thiserror::Error)]
#[error("{span}: {msg}")]
pub struct ElabError {
pub span: Span,
pub msg: String,
}
fn err<T>(span: Span, msg: impl Into<String>) -> Result<T, ElabError> {
Err(ElabError {
span,
msg: msg.into(),
})
}
const NAMES_OVERLAY_CAP: usize = 64;
#[derive(Clone, Debug)]
pub struct Scope<'s> {
names_base: Rc<HashSet<Rc<str>>>,
names_overlay: HashSet<Rc<str>>,
optional_shape: std::collections::HashMap<String, Vec<bool>>,
renames: std::collections::HashMap<String, String>,
version: RustyfiVersion,
store: &'s SymbolStore,
}
impl<'s> Scope<'s> {
pub fn new(store: &'s SymbolStore, names: impl IntoIterator<Item = String>) -> Scope<'s> {
Scope::new_with_version(store, names, RustyfiVersion::V0_0)
}
pub fn new_with_version(
store: &'s SymbolStore,
names: impl IntoIterator<Item = String>,
version: RustyfiVersion,
) -> Scope<'s> {
Scope {
names_base: Rc::new(names.into_iter().map(Rc::from).collect()),
names_overlay: HashSet::new(),
optional_shape: std::collections::HashMap::new(),
renames: std::collections::HashMap::new(),
version,
store,
}
}
fn sym(&self, name: &str) -> Symbol<'s> {
self.store.intern(name)
}
fn with(&self, name: &str) -> Scope<'s> {
let mut s = self.clone();
s.insert(name);
s
}
fn insert(&mut self, name: &str) {
self.names_overlay.insert(Rc::from(name));
self.promote_names();
self.optional_shape.remove(name);
self.renames.remove(name);
}
fn promote_names(&mut self) {
if self.names_overlay.len() < NAMES_OVERLAY_CAP {
return;
}
let mut base = (*self.names_base).clone();
base.extend(self.names_overlay.drain());
self.names_base = Rc::new(base);
}
fn insert_with_shape(&mut self, name: &str, shape: Vec<bool>) {
self.names_overlay.insert(Rc::from(name));
self.promote_names();
if shape.iter().any(|&opt| opt) {
self.optional_shape.insert(name.to_string(), shape);
} else {
self.optional_shape.remove(name);
}
self.renames.remove(name);
}
fn rename(&mut self, local: &str, actual_key: &str) {
self.renames
.insert(local.to_string(), actual_key.to_string());
}
fn resolve(&self, name: &str) -> Symbol<'s> {
self.store.intern(self.resolve_text(name))
}
fn resolve_text<'a>(&'a self, name: &'a str) -> &'a str {
self.renames.get(name).map(|s| s.as_str()).unwrap_or(name)
}
fn contains(&self, name: &str) -> bool {
self.names_overlay.contains(name) || self.names_base.contains(name)
}
fn optional_arity(&self, name: &str) -> usize {
self.optional_shape
.get(name)
.map(|shape| shape.iter().take_while(|&&opt| opt).count())
.unwrap_or(0)
}
fn optional_shape(&self, name: &str) -> &[bool] {
self.optional_shape
.get(name)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
fn names_with_prefix(&self, prefix: &str) -> Vec<String> {
self.names_overlay
.iter()
.chain(self.names_base.iter())
.filter(|n| n.starts_with(prefix))
.map(|n| n.to_string())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect()
}
}
fn scoped_var<'s>(name: &str, span: Span, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
if scope.contains(name) {
Ok(Ast::Var(scope.resolve(name), span))
} else {
err(span, format!("unbound variable '{name}'"))
}
}
#[derive(Clone, Debug)]
pub struct UserTypeDecl {
pub name: String,
pub params: Vec<String>,
pub ctors: Vec<(String, Option<c::TypeExpr>)>,
}
#[derive(Clone, Debug)]
pub struct UserSynonymDecl {
pub name: String,
pub params: Vec<String>,
pub body: c::TypeExpr,
}
enum LoweredTypeDecl {
Variant(UserTypeDecl),
Synonym(UserSynonymDecl),
}
fn lower_type_decl(
decl: &cst::TypeDecl,
mod_path: &[String],
tymap: &HashMap<String, String>,
) -> Vec<LoweredTypeDecl> {
let mut out = Vec::with_capacity(1 + decl.ands.len());
out.push(lower_one_type_clause(
&decl.tyvars,
&decl.name,
&decl.body,
mod_path,
tymap,
));
for a in &decl.ands {
out.push(lower_one_type_clause(
&a.tyvars, &a.name, &a.body, mod_path, tymap,
));
}
out
}
fn lower_one_type_clause(
tyvars: &[rustyfi_syntax::leaf::TypeVarTok],
name: &VarTok,
body: &cst::TypeDeclBody,
mod_path: &[String],
tymap: &HashMap<String, String>,
) -> LoweredTypeDecl {
let params: Vec<String> = tyvars.iter().map(|v| v.name.clone()).collect();
let qname = if name.name.contains('.') {
name.name.clone()
} else {
qualify_key(mod_path, &name.name)
};
match body {
cst::TypeDeclBody::Variant { first, rest, .. } => {
let mut ctors = Vec::with_capacity(1 + rest.len());
let mut push_ctor = |cname: String, payload: Option<&cst::OfType>| {
let ty = payload.map(|o| {
let mut t = o.ty.clone();
qualify_ty(&mut t, tymap);
t
});
ctors.push((cname, ty));
};
push_ctor(first.ctor.name.clone(), first.of_ty.as_ref());
for bv in rest {
push_ctor(bv.def.ctor.name.clone(), bv.def.of_ty.as_ref());
}
LoweredTypeDecl::Variant(UserTypeDecl {
name: qname,
params,
ctors,
})
}
cst::TypeDeclBody::Synonym(ty) => {
let mut b = ty.clone();
qualify_ty(&mut b, tymap);
LoweredTypeDecl::Synonym(UserSynonymDecl {
name: qname,
params,
body: b,
})
}
}
}
fn qualify_ty(ty: &mut c::TypeExpr, map: &HashMap<String, String>) {
if map.is_empty() {
return;
}
match ty {
c::TypeExpr::Fun { opts, dom, cod, .. } => {
for o in opts {
qualify_prod(&mut o.ty, map);
}
qualify_prod(dom, map);
qualify_ty(cod, map);
}
c::TypeExpr::Atom(prod) => qualify_prod(prod, map),
c::TypeExpr::OptRowFun {
opt_dom, dom, cod, ..
} => {
for e in &mut opt_dom.entries {
qualify_ty(&mut e.ty.0, map);
}
qualify_prod(dom, map);
qualify_ty(cod, map);
}
}
}
fn qualify_prod(p: &mut c::TypeProd, map: &HashMap<String, String>) {
qualify_app(&mut p.first, map);
for s in &mut p.rest {
qualify_app(&mut s.ty, map);
}
}
fn qualify_app(a: &mut c::TypeApp, map: &HashMap<String, String>) {
qualify_atom(&mut a.head, map);
for at in &mut a.rest {
qualify_atom(at, map);
}
}
fn qualify_atom(at: &mut c::TypeAtom, map: &HashMap<String, String>) {
match at {
c::TypeAtom::Name(n) => {
if let Some(q) = map.get(&n.name) {
n.name = q.clone();
}
}
c::TypeAtom::Paren { inner, .. } => qualify_ty(&mut inner.0, map),
c::TypeAtom::Record { fields, .. } => {
for f in fields {
qualify_ty(&mut f.ty.0, map);
}
}
c::TypeAtom::RecordOpen { inner, .. } => {
for f in &mut inner.fields {
qualify_ty(&mut f.ty.0, map);
}
}
c::TypeAtom::Cmd { args, .. } => {
for it in args {
for l in &mut it.opt_labels {
qualify_ty(&mut l.ty.0, map);
}
qualify_ty(&mut it.ty.0, map);
}
}
c::TypeAtom::Var(_) | c::TypeAtom::NameMod(_) => {}
}
}
#[derive(Clone, Debug)]
pub struct Program<'s> {
pub type_decls: Vec<UserTypeDecl>,
pub synonym_decls: Vec<UserSynonymDecl>,
pub body: Ast<'s>,
pub store: &'s SymbolStore,
}
pub fn elaborate_program<'s>(
file: &cst::File,
prelude_scope: &Scope<'s>,
) -> Result<Program<'s>, ElabError> {
elaborate_program_with_versions(file, prelude_scope, &HashSet::new(), &HashMap::new(), None)
}
pub fn elaborate_program_with_stages<'s>(
file: &cst::File,
prelude_scope: &Scope<'s>,
stages: &HashMap<usize, crate::types::Stage>,
) -> Result<Program<'s>, ElabError> {
elaborate_program_with_versions(file, prelude_scope, &HashSet::new(), stages, None)
}
pub fn elaborate_program_with_versions<'s>(
file: &cst::File,
prelude_scope: &Scope<'s>,
v006_indices: &HashSet<usize>,
stages: &HashMap<usize, crate::types::Stage>,
wrap_body_version: Option<RustyfiVersion>,
) -> Result<Program<'s>, ElabError> {
let Some(body) = &file.body else {
return err(
Span::default(),
"this file has no document expression - it is a library file",
);
};
let items: Vec<&cst::TopBinding> = file.prelude.iter().collect();
let mut type_decls = Vec::new();
let mut synonym_decls = Vec::new();
let (bindings, _exported, final_scope) = walk_bindings(
&items,
prelude_scope,
&[],
&mut type_decls,
&mut synonym_decls,
&ItemOrigins {
v006: v006_indices,
stages,
},
&HashMap::new(),
)?;
let body_ast = expr(body, &final_scope)?;
let body_ast = match wrap_body_version {
Some(v) => Ast::VersionScope(v, Box::new(body_ast)),
None => body_ast,
};
Ok(Program {
type_decls,
synonym_decls,
body: nest(prelude_scope.store, bindings, body_ast),
store: prelude_scope.store,
})
}
struct ItemOrigins<'a> {
v006: &'a HashSet<usize>,
stages: &'a HashMap<usize, crate::types::Stage>,
}
fn already_staged(value: &Ast<'_>) -> bool {
match value {
Ast::StageScope(..) => true,
Ast::ModuleScope(_, b) | Ast::VersionScope(_, b) => already_staged(b),
_ => false,
}
}
fn stage_wrap_item<'s>(bindings: &mut [Binding<'s>], stage: crate::types::Stage) {
fn wrap<'s>(slot: &mut Ast<'s>, stage: crate::types::Stage) {
if already_staged(slot) {
return;
}
let taken = std::mem::replace(slot, Ast::Unit);
*slot = Ast::StageScope(stage, Box::new(taken));
}
for b in bindings {
match b {
Binding::Let(_, v) | Binding::LetMutable(_, v) | Binding::LetMath(_, v) => {
wrap(v, stage)
}
Binding::LetRec(clauses) => {
for (_, v) in clauses.iter_mut() {
if already_staged(v) {
continue;
}
*v = Rc::new(Ast::StageScope(stage, Box::new((**v).clone())));
}
}
}
}
}
fn binding_stage(
stage: Option<&cst::TopStage>,
version: RustyfiVersion,
authored_v006: bool,
) -> Result<Option<crate::types::Stage>, ElabError> {
let Some(s) = stage else { return Ok(None) };
if authored_v006 || !version.has_per_binding_stage() {
return err(
s.tilde.0,
"a per-binding stage qualifier (`~`) is SATySFi 0.1 syntax (`val ~x = e`) — \
this binding is compiled as 0.0.6, which declares its stage per FILE \
with a `@stage:` header",
);
}
Ok(Some(match s.persistent {
Some(_) => crate::types::Stage::Persistent0,
None => crate::types::Stage::Stage0,
}))
}
fn maybe_v006_scope<'s>(value: Ast<'s>, this_v006: bool) -> Ast<'s> {
if this_v006 {
Ast::VersionScope(RustyfiVersion::V0_0, Box::new(value))
} else {
value
}
}
pub fn elaborate<'s>(file: &cst::File, prelude_scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
Ok(elaborate_program(file, prelude_scope)?.body)
}
fn qualify_key(mod_path: &[String], local: &str) -> String {
if mod_path.is_empty() {
local.to_string()
} else {
format!("{}.{}", mod_path.join("."), local)
}
}
fn direct_cmd_name(item: &cst::SigItem) -> Option<(String, Span)> {
match item {
cst::SigItem::DirectHorzCmd { name, .. } => Some((name.name.clone(), name.span)),
cst::SigItem::DirectVertCmd { name, .. } => Some((name.name.clone(), name.span)),
_ => None,
}
}
enum Binding<'s> {
Let(String, Ast<'s>),
LetRec(Vec<(String, Rc<Ast<'s>>)>),
LetMutable(String, Ast<'s>),
LetMath(String, Ast<'s>),
}
fn nest<'s>(store: &'s SymbolStore, bindings: Vec<Binding<'s>>, tail: Ast<'s>) -> Ast<'s> {
let mut ast = tail;
for b in bindings.into_iter().rev() {
ast = match b {
Binding::Let(name, val) => {
Ast::LetIn(store.intern(&name), Box::new(val), Box::new(ast))
}
Binding::LetRec(bs) => Ast::LetRecIn(
bs.into_iter().map(|(n, v)| (store.intern(&n), v)).collect(),
Box::new(ast),
),
Binding::LetMutable(name, val) => {
Ast::LetMutableIn(store.intern(&name), Box::new(val), Box::new(ast))
}
Binding::LetMath(name, val) => {
Ast::LetMathIn(store.intern(&name), Box::new(val), Box::new(ast))
}
};
}
ast
}
fn export_alias<'s>(
mod_path: &[String],
local: String,
shape: Vec<bool>,
bindings: &mut Vec<Binding<'s>>,
running: &mut Scope<'s>,
exported: &mut Vec<String>,
) {
if mod_path.is_empty() {
running.insert_with_shape(&local, shape);
exported.push(local);
} else {
let qual = qualify_key(mod_path, &local);
let mangled = format!("${qual}");
bindings.push(Binding::Let(
qual.clone(),
Ast::Var(running.sym(&mangled), Span::default()),
));
running.insert_with_shape(&local, shape.clone());
running.rename(&local, &mangled);
running.insert_with_shape(&qual, shape);
exported.push(qual);
}
}
fn push_named_binding<'s>(
mod_path: &[String],
local: String,
value: Ast<'s>,
shape: Vec<bool>,
make_binding: impl FnOnce(String, Ast<'s>) -> Binding<'s>,
bindings: &mut Vec<Binding<'s>>,
running: &mut Scope<'s>,
exported: &mut Vec<String>,
) {
if mod_path.is_empty() {
bindings.push(make_binding(local.clone(), value));
running.insert_with_shape(&local, shape);
exported.push(local);
} else {
let qual = qualify_key(mod_path, &local);
let mangled = format!("${qual}");
let value = Ast::ModuleScope(mod_path.to_vec(), Box::new(value));
bindings.push(make_binding(mangled.clone(), value));
bindings.push(Binding::Let(
qual.clone(),
Ast::Var(running.sym(&mangled), Span::default()),
));
running.insert_with_shape(&local, shape.clone());
running.rename(&local, &mangled);
running.insert_with_shape(&qual, shape);
exported.push(qual);
}
}
fn param_optional_shape(params: &[c::Param]) -> Vec<bool> {
params
.iter()
.map(|p| matches!(p, c::Param::Optional { .. }))
.collect()
}
fn alias_optional_shape<'s>(value: &c::Expr, scope: &Scope<'s>) -> Vec<bool> {
let c::Expr::Ops(chain) = value else {
return Vec::new();
};
if !chain.tail.is_empty() || chain.before.is_some() {
return Vec::new();
}
let a = &chain.head;
if a.minus.is_some()
|| a.excl.is_some()
|| a.stage.is_some()
|| !a.head_accesses.is_empty()
|| !a.args.is_empty()
{
return Vec::new();
}
head_optional_shape(&a.head, scope).to_vec()
}
fn walk_bindings<'s>(
items: &[&cst::TopBinding],
scope: &Scope<'s>,
mod_path: &[String],
type_decls: &mut Vec<UserTypeDecl>,
synonym_decls: &mut Vec<UserSynonymDecl>,
origins: &ItemOrigins<'_>,
tymap: &HashMap<String, String>,
) -> Result<(Vec<Binding<'s>>, Vec<String>, Scope<'s>), ElabError> {
let mut bindings: Vec<Binding<'s>> = Vec::new();
let mut running = scope.clone();
let mut exported: Vec<String> = Vec::new();
let mut level_tymap = tymap.clone();
if !mod_path.is_empty() {
for top in items {
if let cst::TopBinding::Type(decl) = top {
for n in std::iter::once(&decl.name).chain(decl.ands.iter().map(|a| &a.name)) {
if !n.name.contains('.') {
level_tymap.insert(n.name.clone(), qualify_key(mod_path, &n.name));
}
}
}
}
}
for (item_idx, top) in items.iter().enumerate() {
let this_v006 = origins.v006.contains(&item_idx);
let own_stage = match top {
cst::TopBinding::Let(b) => b.stage.as_ref(),
cst::TopBinding::LetRec { stage, .. }
| cst::TopBinding::LetInline { stage, .. }
| cst::TopBinding::LetBlock { stage, .. }
| cst::TopBinding::LetMath { stage, .. }
| cst::TopBinding::LetMutable { stage, .. } => stage.as_ref(),
_ => None,
};
let this_stage = binding_stage(own_stage, scope.version, this_v006)?
.or_else(|| origins.stages.get(&item_idx).copied());
let bindings_before = bindings.len();
match top {
cst::TopBinding::Let(top_let) => {
let top_let_params = params_to_patbots(&top_let.params);
let value = rec_clause_value(&top_let_params, &top_let.value, &[], &running)?;
let value = maybe_v006_scope(value, this_v006);
let mut shape = param_optional_shape(&top_let.params);
if shape.is_empty() && top_let.params.is_empty() {
shape = alias_optional_shape(&top_let.value, &running);
}
push_named_binding(
mod_path,
top_let.name.name.clone(),
value,
shape,
Binding::Let,
&mut bindings,
&mut running,
&mut exported,
);
}
cst::TopBinding::LetPattern { pat, value, .. } => {
let value_ast = expr(value, &running)?;
let value_ast = maybe_v006_scope(value_ast, this_v006);
let lowered_pat = pattern(running.store, pat)?;
let mut names = Vec::new();
collect_pattern_names(running.store, &lowered_pat, &mut names);
let hidden = format!("%patbind.{}.{}", mod_path.join("."), item_idx);
let scrut = if mod_path.is_empty() {
value_ast
} else {
Ast::ModuleScope(mod_path.to_vec(), Box::new(value_ast))
};
bindings.push(Binding::Let(hidden.clone(), scrut));
running.insert_with_shape(&hidden, Vec::new());
for n in &names {
let extract = Ast::Match(
Box::new(Ast::Var(running.sym(&hidden), Span::default())),
vec![MatchArm {
pat: lowered_pat.clone(),
guard: None,
body: Ast::Var(running.sym(n), Span::default()),
}],
);
push_named_binding(
mod_path,
n.to_string(),
extract,
Vec::new(),
Binding::Let,
&mut bindings,
&mut running,
&mut exported,
);
}
}
cst::TopBinding::LetRec { first, ands, .. } => {
let (recs, rec_scope) = rec_bindings(first, ands, &running, mod_path)?;
running = rec_scope;
let names: Vec<String> = std::iter::once(&first.name.name)
.chain(ands.iter().map(|a| &a.binding.name.name))
.cloned()
.collect();
let recs = if this_v006 {
recs.into_iter()
.map(|(n, body)| {
(
n,
Rc::new(Ast::VersionScope(
RustyfiVersion::V0_0,
Box::new((*body).clone()),
)),
)
})
.collect()
} else {
recs
};
let recs: Vec<(String, Rc<Ast<'s>>)> = match this_stage {
Some(st) => recs
.into_iter()
.map(|(n, body)| {
(n, Rc::new(Ast::StageScope(st, Box::new((*body).clone()))))
})
.collect(),
None => recs,
};
let recs: Vec<(String, Rc<Ast<'s>>)> = if mod_path.is_empty() {
recs
} else {
recs.into_iter()
.map(|(n, body)| {
(
n,
Rc::new(Ast::ModuleScope(
mod_path.to_vec(),
Box::new((*body).clone()),
)),
)
})
.collect()
};
bindings.push(Binding::LetRec(recs));
for n in names {
export_alias(
mod_path,
n,
Vec::new(),
&mut bindings,
&mut running,
&mut exported,
);
}
}
cst::TopBinding::LetInline {
ctx,
cmd,
params,
value,
..
} => {
let value_ast =
elaborate_let_inline(ctx.as_ref(), params, value, &running, "read-inline")?;
let value_ast =
maybe_v006_scope(value_ast, this_v006);
push_named_binding(
mod_path,
cmd.name.clone(),
value_ast,
param_optional_shape(params),
Binding::Let,
&mut bindings,
&mut running,
&mut exported,
);
}
cst::TopBinding::LetBlock {
ctx,
cmd,
params,
value,
..
} => {
let value_ast =
elaborate_let_inline(ctx.as_ref(), params, value, &running, "read-block")?;
let value_ast =
maybe_v006_scope(value_ast, this_v006);
push_named_binding(
mod_path,
cmd.name.clone(),
value_ast,
param_optional_shape(params),
Binding::Let,
&mut bindings,
&mut running,
&mut exported,
);
}
cst::TopBinding::LetMath {
cmd,
params,
value,
..
} => {
let value_ast = elaborate_let_math(params, value, &running)?;
let value_ast =
maybe_v006_scope(value_ast, this_v006);
push_named_binding(
mod_path,
cmd.name.clone(),
value_ast,
param_optional_shape(params),
Binding::LetMath,
&mut bindings,
&mut running,
&mut exported,
);
}
cst::TopBinding::Type(decl) => {
for lowered in lower_type_decl(decl, mod_path, &level_tymap) {
match lowered {
LoweredTypeDecl::Variant(v) => type_decls.push(v),
LoweredTypeDecl::Synonym(s) => synonym_decls.push(s),
}
}
}
cst::TopBinding::LetMutable {
name, value, ..
} => {
let value_ast = expr(value, &running)?;
let value_ast =
maybe_v006_scope(value_ast, this_v006);
push_named_binding(
mod_path,
name.name.clone(),
value_ast,
Vec::new(),
Binding::LetMutable,
&mut bindings,
&mut running,
&mut exported,
);
}
cst::TopBinding::Module {
name, sig, decls, ..
} => {
let mut child_path = mod_path.to_vec();
child_path.push(name.name.clone());
let inner_items: Vec<&cst::TopBinding> =
decls.iter().map(|d| d.0.as_ref()).collect();
let inner_v006: HashSet<usize> = if this_v006 {
(0..inner_items.len()).collect()
} else {
HashSet::new()
};
let inner_stages: HashMap<usize, crate::types::Stage> = match this_stage {
Some(st) => (0..inner_items.len()).map(|i| (i, st)).collect(),
None => HashMap::new(),
};
let (inner_bindings, inner_exported, inner_running) = walk_bindings(
&inner_items,
&running,
&child_path,
type_decls,
synonym_decls,
&ItemOrigins {
v006: &inner_v006,
stages: &inner_stages,
},
&level_tymap,
)?;
bindings.extend(inner_bindings);
let self_prefix = if mod_path.is_empty() {
String::new()
} else {
format!("{}.", mod_path.join("."))
};
for q in &inner_exported {
let shape = inner_running.optional_shape(q).to_vec();
running.insert_with_shape(q, shape.clone());
if !self_prefix.is_empty() {
if let Some(rel) = q.strip_prefix(&self_prefix) {
running.insert_with_shape(rel, shape);
running.rename(rel, q);
}
}
}
if let Some(sig_annot) = sig {
for item in &sig_annot.items {
if let Some((local, span)) = direct_cmd_name(item) {
let qual = qualify_key(&child_path, &local);
if !inner_exported.contains(&qual) {
return err(
span,
format!(
"module `{}` signature declares `direct {local} : ..` \
but its `struct .. end` body never defines `{local}`",
name.name
),
);
}
let shape = running.optional_shape(&qual).to_vec();
bindings.push(Binding::Let(
local.clone(),
Ast::Var(running.sym(&qual), Span::default()),
));
running.insert_with_shape(&local, shape);
exported.push(local);
}
}
}
exported.extend(inner_exported);
}
cst::TopBinding::Open { name, .. } => {
let prefix = format!("{}.", name.name);
for q in running.names_with_prefix(&prefix) {
let suffix = q[prefix.len()..].to_string();
let shape = running.optional_shape(&q).to_vec();
bindings.push(Binding::Let(
suffix.clone(),
Ast::Var(running.sym(&q), Span::default()),
));
running.insert_with_shape(&suffix, shape);
}
for q in type_decls
.iter()
.map(|d| &d.name)
.chain(synonym_decls.iter().map(|s| &s.name))
{
if let Some(suffix) = q.strip_prefix(&prefix) {
if !suffix.contains('.') {
level_tymap.insert(suffix.to_string(), q.clone());
}
}
}
}
}
if let Some(st) = this_stage {
stage_wrap_item(&mut bindings[bindings_before..], st);
}
}
Ok((bindings, exported, running))
}
fn curry_cmd_params<'s>(
patbots: &[c::PatBot],
scope: &Scope<'s>,
build_value: impl FnOnce(&Scope<'s>) -> Result<Ast<'s>, ElabError>,
) -> Result<Ast<'s>, ElabError> {
if patbots.iter().all(is_var_patbot) {
let mut inner = scope.clone();
for p in patbots {
inner = inner.with(patbot_var_name(p));
}
let mut value_ast = build_value(&inner)?;
for p in patbots.iter().rev() {
value_ast = Ast::Lambda(scope.sym(patbot_var_name(p)), Rc::new(value_ast));
}
return Ok(value_ast);
}
let pats: Vec<Pattern<'s>> = patbots
.iter()
.map(|p| patbot(scope.store, p))
.collect::<Result<_, _>>()?;
let mut names = Vec::new();
for p in &pats {
collect_pattern_names(scope.store, p, &mut names);
}
let mut inner = scope.clone();
for n in &names {
inner = inner.with(n);
}
let mut value_ast = build_value(&inner)?;
let dummy = Span::default();
for (i, pat) in pats.into_iter().enumerate().rev() {
let fresh = scope.sym(&format!("%cmd_arg{i}"));
value_ast = Ast::Lambda(
fresh,
Rc::new(Ast::Match(
Box::new(Ast::Var(fresh, dummy)),
vec![MatchArm {
pat,
guard: None,
body: value_ast,
}],
)),
);
}
Ok(value_ast)
}
fn elaborate_let_inline<'s>(
ctx: Option<&VarTok>,
params: &[c::Param],
value: &c::Expr,
scope: &Scope<'s>,
reader: &str,
) -> Result<Ast<'s>, ElabError> {
match ctx {
Some(ctxvar) => {
let ctx_scope = scope.with(&ctxvar.name);
let value_ast = curry_cmd_params_v1(params, &ctx_scope, |inner| expr(value, inner))?;
Ok(Ast::Lambda(scope.sym(&ctxvar.name), Rc::new(value_ast)))
}
None => {
const IMPLICIT_CTX: &str = "%context";
let dummy = Span::default();
let ctx_scope = scope.with(IMPLICIT_CTX);
let curried = curry_cmd_params_v1(params, &ctx_scope, |inner| {
let value_ast = expr(value, inner)?;
let read_fn = scoped_var(reader, dummy, inner)?;
let ctx_var = scoped_var(IMPLICIT_CTX, dummy, inner)?;
Ok(Ast::Apply(
Box::new(Ast::Apply(Box::new(read_fn), Box::new(ctx_var))),
Box::new(value_ast),
))
})?;
Ok(Ast::Lambda(scope.sym(IMPLICIT_CTX), Rc::new(curried)))
}
}
}
fn elaborate_let_math<'s>(
params: &[c::Param],
value: &c::Expr,
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
curry_cmd_params_v1(params, scope, |inner| expr(value, inner))
}
fn rec_bindings<'s>(
first: &c::RecBinding,
ands: &[c::AndBinding],
scope: &Scope<'s>,
mod_path: &[String],
) -> Result<(Vec<(String, Rc<Ast<'s>>)>, Scope<'s>), ElabError> {
let all: Vec<&c::RecBinding> = std::iter::once(first)
.chain(ands.iter().map(|a| &a.binding))
.collect();
let mut rec_scope = scope.clone();
for rb in &all {
rec_scope = rec_scope.with(&rb.name.name);
}
let key_of = |name: &str| -> String {
if mod_path.is_empty() {
name.to_string()
} else {
format!("${}", qualify_key(mod_path, name))
}
};
if !mod_path.is_empty() {
for rb in &all {
rec_scope.rename(&rb.name.name, &key_of(&rb.name.name));
}
}
let mut bindings = Vec::with_capacity(all.len());
for rb in all {
let value_ast = rec_clause_value(&rb.params, &rb.value, &rb.extra, &rec_scope)?;
bindings.push((key_of(&rb.name.name), Rc::new(value_ast)));
}
Ok((bindings, rec_scope))
}
fn rec_clause_value<'s>(
params0: &[c::PatBot],
value0: &c::Expr,
extra: &[c::RecClause],
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
let arity = params0.len();
for cl in extra {
if cl.params.len() != arity {
return err(
cl.bar.0,
format!(
"every clause of a multi-clause 'let-rec' binding must bind the \
same number of parameters (expected {arity}, got {})",
cl.params.len()
),
);
}
}
if extra.is_empty() && params0.iter().all(is_var_patbot) {
let mut inner = scope.clone();
for p in params0 {
inner = inner.with(patbot_var_name(p));
}
let mut value_ast = expr(value0, &inner)?;
for p in params0.iter().rev() {
value_ast = Ast::Lambda(scope.sym(patbot_var_name(p)), Rc::new(value_ast));
}
return Ok(value_ast);
}
let fresh: Vec<Symbol<'s>> = (0..arity)
.map(|i| scope.sym(&format!("%rec_arg{i}")))
.collect();
let mut arms = Vec::with_capacity(1 + extra.len());
arms.push(rec_clause_arm(params0, value0, scope)?);
for cl in extra {
arms.push(rec_clause_arm(&cl.params, &cl.value, scope)?);
}
let dummy = Span::default();
let scrutinee = if arity == 1 {
Ast::Var(fresh[0], dummy)
} else {
Ast::Tuple(fresh.iter().map(|f| Ast::Var(*f, dummy)).collect())
};
let mut body = Ast::Match(Box::new(scrutinee), arms);
for f in fresh.iter().rev() {
body = Ast::Lambda(*f, Rc::new(body));
}
Ok(body)
}
fn fun_rows_to_ast<'s>(
kw_span: Span,
opts: &c::CstOptBinders,
param: &c::PatBot,
body: &c::Expr,
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
if !scope.version.has_row_polymorphism() {
return err(
kw_span,
"labeled optional arguments (`?(l = x)`) are SATySFi 0.1 syntax — \
this file is compiled as 0.0.6",
);
}
let mut inner = scope.clone();
for e in &opts.entries {
inner = inner.with(&e.var.name);
}
let body_ast = if is_var_patbot(param) {
let body_scope = inner.with(patbot_var_name(param));
expr(body, &body_scope)?
} else {
let pat = patbot(scope.store, param)?;
let mut names = Vec::new();
collect_pattern_names(scope.store, &pat, &mut names);
let mut body_scope = inner;
for n in &names {
body_scope = body_scope.with(n);
}
expr(body, &body_scope)?
};
lambda_opt_from(scope.store, opts, param, body_ast)
}
fn lambda_opt_from<'s>(
store: &'s SymbolStore,
opts: &c::CstOptBinders,
param: &c::PatBot,
inner_body_ast: Ast<'s>,
) -> Result<Ast<'s>, ElabError> {
let mut opt_pairs: Vec<(String, Symbol<'s>)> = Vec::with_capacity(opts.entries.len());
let mut seen = HashSet::new();
for e in &opts.entries {
if !seen.insert(e.label.name.clone()) {
return err(
e.label.span,
format!(
"duplicate optional label `{}` in one `?(…)` binder list",
e.label.name
),
);
}
opt_pairs.push((e.label.name.clone(), store.intern(&e.var.name)));
}
if is_var_patbot(param) {
Ok(Ast::LambdaOpt {
opts: opt_pairs,
param: store.intern(patbot_var_name(param)),
body: Rc::new(inner_body_ast),
})
} else {
let fresh = store.intern("%opt_arg");
let pat = patbot(store, param)?;
let matched = Ast::Match(
Box::new(Ast::Var(fresh, Span::default())),
vec![MatchArm {
pat,
guard: None,
body: inner_body_ast,
}],
);
Ok(Ast::LambdaOpt {
opts: opt_pairs,
param: fresh,
body: Rc::new(matched),
})
}
}
fn curry_cmd_params_v1<'s>(
params: &[c::Param],
scope: &Scope<'s>,
build_value: impl FnOnce(&Scope<'s>) -> Result<Ast<'s>, ElabError>,
) -> Result<Ast<'s>, ElabError> {
if !params.iter().any(|p| matches!(p, c::Param::Bundled { .. })) {
let patbots = params_to_patbots(params);
return curry_cmd_params(&patbots, scope, build_value);
}
if !scope.version.has_row_polymorphism() {
let bundle_span = params
.iter()
.find_map(|p| match p {
c::Param::Bundled { opts, .. } => Some(opts.q.0),
_ => None,
})
.expect("just checked a `Param::Bundled` entry exists");
return err(
bundle_span,
"labeled optional arguments (`?(l = x)`) are SATySFi 0.1 syntax — \
this file is compiled as 0.0.6",
);
}
let mut inner = scope.clone();
for p in params {
inner = match p {
c::Param::Bundled { opts, body } => {
for e in &opts.entries {
inner = inner.with(&e.var.name);
}
extend_with_patbot(inner, body)?
}
_ => extend_with_patbot(inner, ¶m_to_patbot(p))?,
};
}
let mut value_ast = build_value(&inner)?;
let dummy = Span::default();
for (i, p) in params.iter().enumerate().rev() {
value_ast = match p {
c::Param::Bundled { opts, body } => {
lambda_opt_from(scope.store, opts, body, value_ast)?
}
c::Param::Optional { name, .. } => {
Ast::Lambda(scope.sym(&name.name), Rc::new(value_ast))
}
c::Param::Pat(pat) if is_var_patbot(pat) => {
Ast::Lambda(scope.sym(patbot_var_name(pat)), Rc::new(value_ast))
}
c::Param::Pat(pat) => {
let pp = patbot(scope.store, pat)?;
let fresh = scope.sym(&format!("%cmd_arg{i}"));
Ast::Lambda(
fresh,
Rc::new(Ast::Match(
Box::new(Ast::Var(fresh, dummy)),
vec![MatchArm {
pat: pp,
guard: None,
body: value_ast,
}],
)),
)
}
};
}
Ok(value_ast)
}
fn extend_with_patbot<'s>(scope: Scope<'s>, p: &c::PatBot) -> Result<Scope<'s>, ElabError> {
if is_var_patbot(p) {
Ok(scope.with(patbot_var_name(p)))
} else {
let store = scope.store;
let pat = patbot(store, p)?;
let mut names = Vec::new();
collect_pattern_names(store, &pat, &mut names);
let mut s = scope;
for n in &names {
s = s.with(n);
}
Ok(s)
}
}
fn param_to_patbot(p: &c::Param) -> c::PatBot {
match p {
c::Param::Optional { name, .. } => c::PatBot::Var(name.clone()),
c::Param::Pat(pat) => pat.clone(),
c::Param::Bundled { .. } => {
unreachable!("a `?(l = x)` command-parameter bundle cannot reach `param_to_patbot`")
}
}
}
fn params_to_patbots(params: &[c::Param]) -> Vec<c::PatBot> {
params.iter().map(param_to_patbot).collect()
}
fn is_var_patbot(p: &c::PatBot) -> bool {
matches!(p, c::PatBot::Var(_))
}
fn patbot_var_name(p: &c::PatBot) -> &str {
match p {
c::PatBot::Var(v) => &v.name,
_ => unreachable!("patbot_var_name called on a non-Var PatBot"),
}
}
fn rec_clause_arm<'s>(
params: &[c::PatBot],
value: &c::Expr,
scope: &Scope<'s>,
) -> Result<MatchArm<'s>, ElabError> {
let pats: Vec<Pattern<'s>> = params
.iter()
.map(|p| patbot(scope.store, p))
.collect::<Result<_, _>>()?;
let mut names = Vec::new();
for p in &pats {
collect_pattern_names(scope.store, p, &mut names);
}
let mut inner = scope.clone();
for n in &names {
inner = inner.with(n);
}
let body = expr(value, &inner)?;
let pat = if pats.len() == 1 {
pats.into_iter().next().unwrap()
} else {
Pattern::Tuple(pats)
};
Ok(MatchArm {
pat,
guard: None,
body,
})
}
fn expr<'s>(e: &c::Expr, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
match e {
c::Expr::LetRecIn {
first, ands, body, ..
} => {
let (bindings, rec_scope) = rec_bindings(first, ands, scope, &[])?;
let body_ast = expr(body, &rec_scope)?;
Ok(Ast::LetRecIn(
bindings
.into_iter()
.map(|(n, v)| (scope.sym(&n), v))
.collect(),
Box::new(body_ast),
))
}
c::Expr::LetIn {
name,
params,
value,
body,
..
} => {
let let_in_params = params_to_patbots(params);
let value_ast = rec_clause_value(&let_in_params, value, &[], scope)?;
let mut body_scope = scope.clone();
body_scope.insert_with_shape(&name.name, param_optional_shape(params));
let body_ast = expr(body, &body_scope)?;
Ok(Ast::LetIn(
scope.sym(&name.name),
Box::new(value_ast),
Box::new(body_ast),
))
}
c::Expr::LetPatternIn {
pat, value, body, ..
} => {
let value_ast = expr(value, scope)?;
let lowered_pat = pattern(scope.store, pat)?;
let mut names = Vec::new();
collect_pattern_names(scope.store, &lowered_pat, &mut names);
let mut inner = scope.clone();
for n in &names {
inner = inner.with(n);
}
let body_ast = expr(body, &inner)?;
Ok(Ast::Match(
Box::new(value_ast),
vec![MatchArm {
pat: lowered_pat,
guard: None,
body: body_ast,
}],
))
}
c::Expr::If {
cond,
then_branch,
else_branch,
..
} => Ok(Ast::IfThenElse(
Box::new(expr(cond, scope)?),
Box::new(expr(then_branch, scope)?),
Box::new(expr(else_branch, scope)?),
)),
c::Expr::Fun {
kw, params, body, ..
} => {
if params.is_empty() {
return err(kw.0, "'fun' needs at least one parameter");
}
rec_clause_value(params, body, &[], scope)
}
c::Expr::FunRows {
kw,
opts,
param,
body,
..
} => fun_rows_to_ast(kw.0, opts, param, body, scope),
c::Expr::Match {
scrutinee,
first,
rest,
..
} => {
let scrut = expr(scrutinee, scope)?;
let mut arms = Vec::with_capacity(1 + rest.len());
arms.push(match_arm(first, scope)?);
for bar in rest {
arms.push(match_arm(&bar.arm, scope)?);
}
Ok(Ast::Match(Box::new(scrut), arms))
}
c::Expr::LetMutableIn {
name, init, body, ..
} => {
let init_ast = expr(init, scope)?;
let inner = scope.with(&name.name);
let body_ast = expr(body, &inner)?;
Ok(Ast::LetMutableIn(
scope.sym(&name.name),
Box::new(init_ast),
Box::new(body_ast),
))
}
c::Expr::LetMathIn {
cmd,
params,
value,
body,
..
} => {
let value_ast = elaborate_let_math(params, value, scope)?;
let mut body_scope = scope.clone();
body_scope.insert_with_shape(&cmd.name, param_optional_shape(params));
let body_ast = expr(body, &body_scope)?;
Ok(Ast::LetMathIn(
scope.sym(&cmd.name),
Box::new(value_ast),
Box::new(body_ast),
))
}
c::Expr::OpenIn { name, body, .. } => {
open_module(&name.name, name.span, scope, |s| expr(body, s))
}
c::Expr::WhileDo { cond, body, .. } => Ok(Ast::WhileDo(
Box::new(expr(cond, scope)?),
Box::new(expr(body, scope)?),
)),
c::Expr::Overwrite { name, value, .. } => {
if !scope.contains(&name.name) {
return err(
name.span,
format!("unbound mutable variable '{}'", name.name),
);
}
Ok(Ast::Overwrite(
scope.resolve(&name.name),
name.span,
Box::new(expr(value, scope)?),
))
}
c::Expr::Ops(chain) => op_chain(chain, scope),
}
}
#[derive(Clone, Copy)]
enum Assoc {
Left,
Right,
}
fn op_prec(tok: &Token) -> (u8, Assoc) {
match tok {
Token::BinopBar(_) => (1, Assoc::Left),
Token::BinopAmp(_) => (2, Assoc::Left),
Token::BinopEq(_) | Token::BinopGt(_) | Token::BinopLt(_) => (3, Assoc::Right),
Token::BinopHat(_) | Token::Cons => (4, Assoc::Right),
Token::BinopPlus(_) | Token::BinopMinus(_) | Token::ExactMinus => (5, Assoc::Left),
Token::BinopTimes(_) | Token::ExactTimes | Token::BinopDivides(_) | Token::Mod => {
(6, Assoc::Right)
}
_ => unreachable!("BinOpTok::parse only ever matches the operator tokens listed above"),
}
}
fn op_chain<'s>(chain: &c::OpChain, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let head_ast = app_expr(&chain.head, scope)?;
let folded = if chain.tail.is_empty() {
head_ast
} else {
let mut atoms: VecDeque<Ast<'s>> = VecDeque::with_capacity(chain.tail.len() + 1);
atoms.push_back(head_ast);
let mut ops: VecDeque<(String, Span, Token)> = VecDeque::with_capacity(chain.tail.len());
for rhs in &chain.tail {
let text = rhs.op.op_text();
if text != "|>" && !scope.contains(&text) {
return err(rhs.op.span, format!("unbound operator '{text}'"));
}
ops.push_back((
scope.resolve_text(&text).to_string(),
rhs.op.span,
rhs.op.tok.clone(),
));
atoms.push_back(app_expr(&rhs.rhs, scope)?);
}
climb(&mut atoms, &mut ops, 0, scope)
};
match &chain.before {
Some(bt) => Ok(Ast::Sequential(
Box::new(folded),
Box::new(expr(&bt.body, scope)?),
)),
None => Ok(folded),
}
}
fn climb<'s>(
atoms: &mut VecDeque<Ast<'s>>,
ops: &mut VecDeque<(String, Span, Token)>,
min_prec: u8,
scope: &Scope<'s>,
) -> Ast<'s> {
let mut lhs = atoms
.pop_front()
.expect("one more atom than consumed operators");
while let Some((_, _, tok)) = ops.front() {
let (prec, assoc) = op_prec(tok);
if prec < min_prec {
break;
}
let (text, span, _) = ops.pop_front().unwrap();
let next_min = match assoc {
Assoc::Left => prec + 1,
Assoc::Right => prec,
};
let rhs = climb(atoms, ops, next_min, scope);
lhs = if text == "|>" {
Ast::Apply(Box::new(rhs), Box::new(lhs))
} else {
Ast::Apply(
Box::new(Ast::Apply(
Box::new(Ast::Var(scope.sym(&text), span)),
Box::new(lhs),
)),
Box::new(rhs),
)
};
}
lhs
}
fn app_expr<'s>(a: &c::AppExpr, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
if a.minus.is_none() && a.excl.is_none() && a.stage.is_none() && a.head_accesses.is_empty() && !a.args.is_empty() {
if let c::Atomic::Var(v) = &a.head {
if v.name == "not" && scope.contains("not") && scope.resolve_text("not") == "not" {
let not_fn = scoped_var("not", v.span, scope)?;
let mut inner = app_arg_to_ast(&a.args[0], scope)?;
for rest in &a.args[1..] {
inner = apply_one_arg(inner, rest, scope)?;
}
return Ok(Ast::Apply(Box::new(not_fn), Box::new(inner)));
}
}
}
let ast = if a.excl.is_none() && a.stage.is_none() && a.head_accesses.is_empty() {
if let c::Atomic::Ctor(ctor) = &a.head {
let mut args_iter = a.args.iter();
match args_iter.next() {
Some(first) => {
let payload = app_arg_to_ast(first, scope)?;
let mut ast = Ast::Ctor(ctor.name.clone(), Some(Box::new(payload)));
for rest in args_iter {
ast = apply_one_arg(ast, rest, scope)?;
}
ast
}
None => Ast::Ctor(ctor.name.clone(), None),
}
} else {
app_chain_generic(a, scope)?
}
} else {
app_chain_generic(a, scope)?
};
match &a.minus {
Some(m) => {
let minus = scoped_var("-", m.0, scope)?;
Ok(Ast::Apply(
Box::new(Ast::Apply(Box::new(minus), Box::new(Ast::Int(0)))),
Box::new(ast),
))
}
None => Ok(ast),
}
}
fn app_chain_generic<'s>(a: &c::AppExpr, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let mut ast =
atomic_head_with_excl(&a.head, &a.head_accesses, a.excl.as_ref(), a.stage.as_ref(), scope)?;
let shape: &[bool] = if a.excl.is_none() && a.head_accesses.is_empty() && !a.args.is_empty() {
head_optional_shape(&a.head, scope)
} else {
&[]
};
let mut args_iter = a.args.iter().peekable();
let mut pos = 0usize;
while pos < shape.len() {
if shape[pos] {
match args_iter.peek() {
Some(c::AppArg::Optional { .. }) | Some(c::AppArg::Omission(_)) => {
let arg = args_iter.next().unwrap();
ast = Ast::Apply(Box::new(ast), Box::new(app_arg_to_ast(arg, scope)?));
}
Some(_) => {
ast = Ast::Apply(Box::new(ast), Box::new(Ast::Ctor("None".to_string(), None)));
}
None => break,
}
} else {
match args_iter.next() {
Some(arg) => ast = apply_one_arg(ast, arg, scope)?,
None => break,
}
}
pos += 1;
}
for arg in args_iter {
ast = apply_one_arg(ast, arg, scope)?;
}
Ok(ast)
}
fn apply_one_arg<'s>(
func: Ast<'s>,
arg: &c::AppArg,
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
match arg {
c::AppArg::Bundled {
opts,
excl,
atom,
accesses,
} => {
let opt_args = elaborate_opt_args(opts, scope)?;
let arg_ast = atomic_head_with_excl(atom, accesses, excl.as_ref(), None, scope)?;
Ok(Ast::ApplyOpt {
func: Box::new(func),
opts: opt_args,
arg: Box::new(arg_ast),
})
}
c::AppArg::BundledCtor { opts, ctor } => {
let opt_args = elaborate_opt_args(opts, scope)?;
Ok(Ast::ApplyOpt {
func: Box::new(func),
opts: opt_args,
arg: Box::new(Ast::Ctor(ctor.name.clone(), None)),
})
}
_ => Ok(Ast::Apply(
Box::new(func),
Box::new(app_arg_to_ast(arg, scope)?),
)),
}
}
fn elaborate_opt_args<'s>(
opts: &c::CstOptArgs,
scope: &Scope<'s>,
) -> Result<Vec<(String, Ast<'s>)>, ElabError> {
if !scope.version.has_row_polymorphism() {
return err(
opts.q.0,
"labeled optional arguments (`?(l = e)`) are SATySFi 0.1 syntax — \
this file is compiled as 0.0.6",
);
}
let mut out: Vec<(String, Ast<'s>)> = Vec::with_capacity(opts.entries.len());
let mut seen = HashSet::new();
for e in &opts.entries {
if !seen.insert(e.label.name.clone()) {
return err(
e.label.span,
format!(
"duplicate optional label `{}` in one `?(…)` bundle",
e.label.name
),
);
}
out.push((e.label.name.clone(), expr(&e.value.0, scope)?));
}
Ok(out)
}
fn head_optional_shape<'s, 'a>(head: &c::Atomic, scope: &'a Scope<'s>) -> &'a [bool] {
match head {
c::Atomic::Var(v) => scope.optional_shape(&v.name),
c::Atomic::VarWithMod(v) => scope.optional_shape(&qualify_key(&v.mods, &v.name)),
_ => &[],
}
}
fn atomic_head_with_excl<'s>(
head: &c::Atomic,
accesses: &[c::AccessSeg],
excl: Option<&UnopExclamTok>,
stage: Option<&c::StagePrefix>,
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
let mut ast = atomic(head, scope)?;
for acc in accesses {
ast = Ast::AccessField(Box::new(ast), acc.label.name.clone(), acc.label.span);
}
if let Some(e) = excl {
let deref_fn = scoped_var(&e.text, e.span, scope)?;
ast = Ast::Apply(Box::new(deref_fn), Box::new(ast));
}
Ok(match stage {
Some(c::StagePrefix::Next(_)) => Ast::Next(Box::new(ast)),
Some(c::StagePrefix::Prev(_)) => Ast::Prev(Box::new(ast)),
None => ast,
})
}
fn app_arg_to_ast<'s>(arg: &c::AppArg, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
match arg {
c::AppArg::Optional { value, .. } => {
let inner = atomic(value, scope)?;
Ok(Ast::Ctor("Some".to_string(), Some(Box::new(inner))))
}
c::AppArg::Omission(_) => Ok(Ast::Ctor("None".to_string(), None)),
c::AppArg::Atom {
stage,
excl,
atom,
accesses,
} => atomic_head_with_excl(atom, accesses, excl.as_ref(), stage.as_ref(), scope),
c::AppArg::Ctor(ctor) => Ok(Ast::Ctor(ctor.name.clone(), None)),
c::AppArg::Bundled { opts, .. } | c::AppArg::BundledCtor { opts, .. } => err(
opts.q.0,
"a `?(l = e)` labeled-optional bundle cannot be used as a plain \
argument value here",
),
}
}
fn open_module<'s>(
module_name: &str,
name_span: Span,
scope: &Scope<'s>,
body: impl FnOnce(&Scope<'s>) -> Result<Ast<'s>, ElabError>,
) -> Result<Ast<'s>, ElabError> {
let prefix = format!("{module_name}.");
let matches = scope.names_with_prefix(&prefix);
let mut inner = scope.clone();
for q in &matches {
let shape = scope.optional_shape(q).to_vec();
inner.insert_with_shape(&q[prefix.len()..], shape);
}
let body_ast = body(&inner)?;
let mut ast = body_ast;
for q in matches.into_iter().rev() {
let suffix = q[prefix.len()..].to_string();
let key = scope.resolve(&q);
ast = Ast::LetIn(
scope.sym(&suffix),
Box::new(Ast::Var(key, name_span)),
Box::new(ast),
);
}
Ok(ast)
}
fn atomic<'s>(a: &c::Atomic, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
match a {
c::Atomic::Length(l) => match Length::from_unit(l.value, &l.unit) {
Some(len) => Ok(Ast::Length(len)),
None => err(l.span, format!("unknown length unit '{}'", l.unit)),
},
c::Atomic::Float(f) => Ok(Ast::Float(f.value)),
c::Atomic::Int(i) => Ok(Ast::Int(i.value)),
c::Atomic::Literal(l) => Ok(Ast::Str(omit_spaces(l.omit_pre, l.omit_post, &l.body))),
c::Atomic::True(_) => Ok(Ast::Bool(true)),
c::Atomic::False(_) => Ok(Ast::Bool(false)),
c::Atomic::Ctor(ctor) => Ok(Ast::Ctor(ctor.name.clone(), None)),
c::Atomic::Var(v) => scoped_var(&v.name, v.span, scope),
c::Atomic::VarWithMod(tok) => {
scoped_var(&qualify_key(&tok.mods, &tok.name), tok.span, scope)
}
c::Atomic::OpRef(op) => scoped_var(&op.name, op.span, scope),
c::Atomic::Command { name, .. } => {
let (key, span) = horz_cmd_key(name);
scoped_var(&key, span, scope)
}
c::Atomic::Unit { .. } => Ok(Ast::Unit),
c::Atomic::Paren { inner, .. } => paren_body(inner, scope),
c::Atomic::OpenModule { grp, body } => {
open_module(&grp.open.name, grp.open.span, scope, |s| {
paren_body(body, s)
})
}
c::Atomic::Record { body, .. } => record_body_to_ast(body, scope),
c::Atomic::List { items, .. } => {
let mut out = Vec::with_capacity(items.len());
for it in items {
out.push(expr(&it.value, scope)?);
}
Ok(Ast::List(out))
}
c::Atomic::InlineText { elems, .. } => inline_text_ast(elems, scope),
c::Atomic::BlockText { elems, .. } => {
Ok(Ast::BlockText(Rc::new(block_elems(elems, scope)?)))
}
c::Atomic::MathText { elems, .. } => math_block_ast(elems, scope),
}
}
fn paren_body<'s>(pb: &c::ParenBody, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let first = expr(&pb.first, scope)?;
if pb.rest.is_empty() {
Ok(first)
} else {
let mut items = Vec::with_capacity(pb.rest.len() + 1);
items.push(first);
for r in &pb.rest {
items.push(expr(&r.value, scope)?);
}
Ok(Ast::Tuple(items))
}
}
fn record_body_to_ast<'s>(body: &c::RecordBody, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
match body {
c::RecordBody::Fields(fields) => {
let mut out = Vec::with_capacity(fields.len());
for f in fields {
out.push((f.name.name.clone(), expr(&f.value, scope)?));
}
Ok(Ast::Record(out))
}
c::RecordBody::Update { base, fields, .. } => {
let mut ast = expr(base, scope)?;
for f in fields {
let v = expr(&f.value, scope)?;
ast = Ast::UpdateField(Box::new(ast), f.name.name.clone(), Box::new(v));
}
Ok(ast)
}
}
}
fn pattern<'s>(store: &'s SymbolStore, p: &c::Pattern) -> Result<Pattern<'s>, ElabError> {
let head = pat_cons(store, &p.head)?;
match &p.as_clause {
Some(ac) => Ok(Pattern::As(Box::new(head), store.intern(&ac.name.name))),
None => Ok(head),
}
}
fn pat_cons<'s>(store: &'s SymbolStore, pc: &c::PatCons) -> Result<Pattern<'s>, ElabError> {
let mut segs: Vec<&c::PatBot> = Vec::with_capacity(pc.tail.len() + 1);
segs.push(&pc.head);
for seg in &pc.tail {
segs.push(&seg.tail);
}
let mut iter = segs.into_iter().rev();
let last = iter.next().expect("PatCons always has a head");
let mut acc = patbot(store, last)?;
for pb in iter {
acc = Pattern::Cons(Box::new(patbot(store, pb)?), Box::new(acc));
}
Ok(acc)
}
fn patbot<'s>(store: &'s SymbolStore, pb: &c::PatBot) -> Result<Pattern<'s>, ElabError> {
match pb {
c::PatBot::CtorApplied { ctor, arg } => Ok(Pattern::Ctor(
ctor.name.clone(),
Some(Box::new(patbot(store, arg)?)),
)),
c::PatBot::Ctor(ctor) => Ok(Pattern::Ctor(ctor.name.clone(), None)),
c::PatBot::Int(i) => Ok(Pattern::Int(i.value)),
c::PatBot::True(_) => Ok(Pattern::Bool(true)),
c::PatBot::False(_) => Ok(Pattern::Bool(false)),
c::PatBot::Str(l) => Ok(Pattern::Str(l.body.clone())),
c::PatBot::Wild(_) => Ok(Pattern::Wild),
c::PatBot::Var(v) => Ok(Pattern::Var(store.intern(&v.name))),
c::PatBot::Unit { .. } => Ok(Pattern::Unit),
c::PatBot::Paren { inner, .. } => {
let first = pattern(store, &inner.first)?;
if inner.rest.is_empty() {
Ok(first)
} else {
let mut items = Vec::with_capacity(inner.rest.len() + 1);
items.push(first);
for r in &inner.rest {
items.push(pattern(store, &r.value)?);
}
Ok(Pattern::Tuple(items))
}
}
c::PatBot::List { items, .. } => {
let mut acc = Pattern::EmptyList;
for it in items.iter().rev() {
acc = Pattern::Cons(Box::new(pattern(store, &it.value)?), Box::new(acc));
}
Ok(acc)
}
}
}
fn collect_pattern_names<'s>(store: &'s SymbolStore, p: &Pattern<'s>, out: &mut Vec<&'s str>) {
match p {
Pattern::Var(n) => out.push(store.resolve(*n)),
Pattern::As(inner, n) => {
collect_pattern_names(store, inner, out);
out.push(store.resolve(*n));
}
Pattern::Tuple(ps) => {
for p in ps {
collect_pattern_names(store, p, out);
}
}
Pattern::Cons(head, tail) => {
collect_pattern_names(store, head, out);
collect_pattern_names(store, tail, out);
}
Pattern::Ctor(_, Some(inner)) => collect_pattern_names(store, inner, out),
Pattern::Wild
| Pattern::Unit
| Pattern::Bool(_)
| Pattern::Int(_)
| Pattern::Str(_)
| Pattern::EmptyList
| Pattern::Ctor(_, None) => {}
}
}
fn match_arm<'s>(arm: &c::MatchArm, scope: &Scope<'s>) -> Result<MatchArm<'s>, ElabError> {
let pat = pattern(scope.store, &arm.pat)?;
let mut names = Vec::new();
collect_pattern_names(scope.store, &pat, &mut names);
let mut inner = scope.clone();
for n in &names {
inner = inner.with(n);
}
let guard = match &arm.guard {
Some(g) => Some(expr(&g.cond, &inner)?),
None => None,
};
let body = expr(&arm.body, &inner)?;
Ok(MatchArm { pat, guard, body })
}
fn horz_cmd_key(name: &AnyHorzCmdTok) -> (String, Span) {
match name {
AnyHorzCmdTok::Plain(t) => (t.name.clone(), t.span),
AnyHorzCmdTok::Mod(t) => (qualify_key(&t.mods, &t.name), t.span),
}
}
fn vert_cmd_key(name: &AnyVertCmdTok) -> (String, Span) {
match name {
AnyVertCmdTok::Plain(t) => (t.name.clone(), t.span),
AnyVertCmdTok::Mod(t) => (qualify_key(&t.mods, &t.name), t.span),
}
}
fn math_cmd_key(name: &AnyMathCmdTok) -> (String, Span) {
match name {
AnyMathCmdTok::Plain(t) => (t.name.clone(), t.span),
AnyMathCmdTok::Mod(t) => (qualify_key(&t.mods, &t.name), t.span),
}
}
fn inline_text_ast<'s>(elems: &[c::InlineElem], scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
if matches!(elems.first(), Some(c::InlineElem::Sep(_))) {
return inline_text_list(elems, scope);
}
if elems
.iter()
.any(|e| matches!(e, c::InlineElem::ItemBullet(_)))
{
itemize(elems, scope)
} else {
Ok(Ast::InlineText(Rc::new(inline_elems(elems, scope)?)))
}
}
fn inline_text_list<'s>(elems: &[c::InlineElem], scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let mut groups: Vec<&[c::InlineElem]> = Vec::new();
let mut start = 0usize;
for (i, e) in elems.iter().enumerate() {
if matches!(e, c::InlineElem::Sep(_)) {
groups.push(&elems[start..i]);
start = i + 1;
}
}
groups.push(&elems[start..]);
if groups.first().is_some_and(|g| g.is_empty()) {
groups.remove(0);
}
if groups.last().is_some_and(|g| g.is_empty()) {
groups.pop();
}
let mut items = Vec::with_capacity(groups.len());
for g in groups {
items.push(inline_text_ast(g, scope)?);
}
Ok(Ast::List(items))
}
fn inline_elems<'s>(
elems: &[c::InlineElem],
scope: &Scope<'s>,
) -> Result<Vec<IText<'s>>, ElabError> {
let mut out = Vec::new();
let mut text = String::new();
for el in elems {
match el {
c::InlineElem::Char(ch) => text.push_str(&ch.text),
c::InlineElem::CodeText(t) => {
if !text.is_empty() {
out.push(IText::Text(std::mem::take(&mut text)));
}
out.push(IText::CodeText(t.text.clone()));
}
c::InlineElem::Space(_) => text.push(' '),
c::InlineElem::Break(_) => text.push('\n'),
c::InlineElem::Cmd { name, tail } => {
if !text.is_empty() {
out.push(IText::Text(std::mem::take(&mut text)));
}
let (key, span) = horz_cmd_key(name);
if !scope.contains(&key) {
return err(span, format!("unbound inline command '{key}'"));
}
out.push(IText::Cmd {
name: scope.resolve(&key),
span,
args: cmd_args(tail, scope, scope.optional_shape(&key))?,
});
}
c::InlineElem::Embed { var, .. } => {
if !text.is_empty() {
out.push(IText::Text(std::mem::take(&mut text)));
}
let key = qualify_key(&var.mods, &var.name);
if !scope.contains(&key) {
return err(var.span, format!("unbound variable '{key}'"));
}
out.push(IText::Embed {
expr: Ast::Var(scope.resolve(&key), var.span),
span: var.span,
});
}
c::InlineElem::EmbedMath { mgrp, elems } => {
if !text.is_empty() {
out.push(IText::Text(std::mem::take(&mut text)));
}
if let Some(first) = elems.first() {
if let c::MathBot::Sep(tok) = &first.base {
return err(tok.0, "a '|'-separated math list cannot be embedded directly in inline text: `${| … |}` here would be a `math list`, but an embedded formula must be a single `math`");
}
}
let span = mgrp.open.0.unite(mgrp.close.0);
out.push(IText::EmbedMath {
elems: Rc::new(lower_math_elems(elems, scope)?),
span,
});
}
c::InlineElem::ItemBullet(tok) => {
return err(
tok.span,
"unexpected itemize bullet '*' outside a bullet list",
);
}
c::InlineElem::Sep(tok) => {
return err(tok.0, "'|' separator is not supported here yet");
}
}
}
if !text.is_empty() {
out.push(IText::Text(text));
}
Ok(out)
}
fn block_elems<'s>(elems: &[c::BlockElem], scope: &Scope<'s>) -> Result<Vec<BText<'s>>, ElabError> {
let mut out = Vec::with_capacity(elems.len());
for el in elems {
match el {
c::BlockElem::Cmd { name, tail } => {
let (key, span) = vert_cmd_key(name);
if !scope.contains(&key) {
return err(span, format!("unbound block command '{key}'"));
}
out.push(BText::Cmd {
name: scope.resolve(&key),
span,
args: cmd_args(tail, scope, scope.optional_shape(&key))?,
});
}
c::BlockElem::Embed { var, .. } => {
let key = qualify_key(&var.mods, &var.name);
if !scope.contains(&key) {
return err(var.span, format!("unbound variable '{key}'"));
}
out.push(BText::Embed {
expr: Ast::Var(scope.resolve(&key), var.span),
span: var.span,
});
}
}
}
Ok(out)
}
fn cmd_args<'s>(
tail: &c::CmdTail,
scope: &Scope<'s>,
shape: &[bool],
) -> Result<Vec<CmdArg<'s>>, ElabError> {
let args: Vec<&c::AppArg> = match tail {
c::CmdTail::Semi(_) => Vec::new(),
c::CmdTail::Args { first, rest, .. } => {
let mut v: Vec<&c::AppArg> = Vec::with_capacity(1 + rest.len());
v.push(first);
for a in rest {
v.push(a);
}
v
}
};
let mut out = Vec::with_capacity(args.len().max(shape.len()));
let mut args_iter = args.into_iter().peekable();
let mut pos = 0usize;
while pos < shape.len() {
if shape[pos] {
match args_iter.peek() {
Some(c::AppArg::Optional { .. }) | Some(c::AppArg::Omission(_)) => {
out.push(CmdArg {
opts: Vec::new(),
arg: app_arg_to_ast(args_iter.next().unwrap(), scope)?,
});
}
_ => out.push(CmdArg {
opts: Vec::new(),
arg: Ast::Ctor("None".to_string(), None),
}),
}
} else {
match args_iter.next() {
Some(a) => out.push(cmd_arg_to_ast(a, scope)?),
None => break,
}
}
pos += 1;
}
for a in args_iter {
out.push(cmd_arg_to_ast(a, scope)?);
}
Ok(out)
}
fn cmd_arg_to_ast<'s>(arg: &c::AppArg, scope: &Scope<'s>) -> Result<CmdArg<'s>, ElabError> {
match arg {
c::AppArg::Bundled {
opts,
excl,
atom,
accesses,
} => Ok(CmdArg {
opts: elaborate_opt_args(opts, scope)?,
arg: atomic_head_with_excl(atom, accesses, excl.as_ref(), None, scope)?,
}),
c::AppArg::BundledCtor { opts, ctor } => Ok(CmdArg {
opts: elaborate_opt_args(opts, scope)?,
arg: Ast::Ctor(ctor.name.clone(), None),
}),
_ => Ok(CmdArg {
opts: Vec::new(),
arg: app_arg_to_ast(arg, scope)?,
}),
}
}
struct ItemNode<'s> {
text: Ast<'s>,
children: Vec<ItemNode<'s>>,
}
fn inline_elem_span(el: &c::InlineElem) -> Span {
match el {
c::InlineElem::Char(t) => t.span,
c::InlineElem::CodeText(t) => t.span,
c::InlineElem::Space(t) => t.0,
c::InlineElem::Break(t) => t.0,
c::InlineElem::Embed { var, .. } => var.span,
c::InlineElem::EmbedMath { mgrp, .. } => mgrp.open.0.unite(mgrp.close.0),
c::InlineElem::Cmd { name, .. } => horz_cmd_key(name).1,
c::InlineElem::ItemBullet(t) => t.span,
c::InlineElem::Sep(t) => t.0,
}
}
fn itemize<'s>(elems: &[c::InlineElem], scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let mut i = 0;
while i < elems.len() && !matches!(elems[i], c::InlineElem::ItemBullet(_)) {
i += 1;
}
if i != 0 {
return err(
inline_elem_span(&elems[0]),
"content before the first itemize bullet '*' is not supported",
);
}
let mut segments: Vec<(usize, Span, &[c::InlineElem])> = Vec::new();
while i < elems.len() {
let (depth, span) = match &elems[i] {
c::InlineElem::ItemBullet(tok) => (tok.depth, tok.span),
_ => unreachable!("loop invariant: elems[i] is always an ItemBullet here"),
};
let start = i + 1;
let mut j = start;
while j < elems.len() && !matches!(elems[j], c::InlineElem::ItemBullet(_)) {
j += 1;
}
segments.push((depth, span, &elems[start..j]));
i = j;
}
let mut root = ItemNode {
text: Ast::InlineText(Rc::new(Vec::new())),
children: Vec::new(),
};
let mut crrntdp = 0usize;
for (depth, span, content) in segments {
if depth > crrntdp + 1 {
return err(span, format!("illegal item depth {depth} after {crrntdp}"));
}
let text_ast = Ast::InlineText(Rc::new(inline_elems(content, scope)?));
insert_last(&mut root, 1, depth, text_ast);
crrntdp = depth;
}
Ok(item_node_to_ast(root))
}
fn insert_last<'s>(node: &mut ItemNode<'s>, i: usize, depth: usize, new_text: Ast<'s>) {
if node.children.is_empty() {
node.children.push(ItemNode {
text: new_text,
children: Vec::new(),
});
return;
}
if i < depth {
insert_last(node.children.last_mut().unwrap(), i + 1, depth, new_text);
} else {
node.children.push(ItemNode {
text: new_text,
children: Vec::new(),
});
}
}
fn item_node_to_ast<'s>(node: ItemNode<'s>) -> Ast<'s> {
let children = Ast::List(node.children.into_iter().map(item_node_to_ast).collect());
Ast::Ctor(
"Item".to_string(),
Some(Box::new(Ast::Tuple(vec![node.text, children]))),
)
}
fn lower_math_elems<'s>(
elems: &[cst::MathErased],
scope: &Scope<'s>,
) -> Result<Vec<MathElem<'s>>, ElabError> {
elems.iter().map(|e| math_elem_cst(e, scope)).collect()
}
fn math_block_ast<'s>(elems: &[cst::MathErased], scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
let leading_sep = matches!(elems.first(), Some(e) if matches!(&e.base, c::MathBot::Sep(_)));
if !leading_sep {
return Ok(Ast::MathText(Rc::new(lower_math_elems(elems, scope)?)));
}
for e in elems {
if let c::MathBot::Sep(tok) = &e.base {
if !e.scripts.is_empty() {
return err(
tok.0,
"a '|' math-list separator cannot carry a script ('^'/'_'/primes)",
);
}
}
}
if !matches!(elems.last(), Some(e) if matches!(&e.base, c::MathBot::Sep(_))) {
let c::MathBot::Sep(first) = &elems[0].base else {
unreachable!()
};
return err(
first.0,
"a '|'-separated math list must end with a trailing '|' (write `${| a | b |}`)",
);
}
let mut segments: Vec<Ast<'s>> = Vec::new();
let mut seg_start = 1usize;
for (i, e) in elems.iter().enumerate().skip(1) {
if matches!(&e.base, c::MathBot::Sep(_)) {
let seg = &elems[seg_start..i];
segments.push(Ast::MathText(Rc::new(lower_math_elems(seg, scope)?)));
seg_start = i + 1;
}
}
Ok(Ast::List(segments))
}
fn math_elem_cst<'s>(m: &c::MathElemCst, scope: &Scope<'s>) -> Result<MathElem<'s>, ElabError> {
let base = math_bot(&m.base, scope)?;
fold_math_scripts(base, &m.scripts, scope)
}
fn math_bot<'s>(b: &c::MathBot, scope: &Scope<'s>) -> Result<MathElem<'s>, ElabError> {
match b {
c::MathBot::Cmd { name, args } => {
let (key, span) = math_cmd_key(name);
if !scope.contains(&key) {
return err(span, format!("unbound math command '{key}'"));
}
let leading = scope.optional_arity(&key);
let mut arg_asts = Vec::with_capacity(args.len().max(leading));
let mut args_iter = args.iter().peekable();
let mut supplied = 0;
while supplied < leading {
match args_iter.peek() {
Some(c::MathArg::Optional { .. }) | Some(c::MathArg::Omission(_)) => {
arg_asts.push(math_arg_to_ast(args_iter.next().unwrap(), scope)?);
supplied += 1;
}
_ => break,
}
}
for _ in supplied..leading {
arg_asts.push(Ast::Ctor("None".to_string(), None));
}
for a in args_iter {
arg_asts.push(math_arg_to_ast(a, scope)?);
}
Ok(MathElem::Cmd {
name: scope.resolve(&key),
span,
args: arg_asts
.into_iter()
.map(|arg| CmdArg { opts: Vec::new(), arg })
.collect(),
})
}
c::MathBot::Chars(tok) => Ok(MathElem::Chars(tok.text.clone())),
c::MathBot::Embed(tok) => {
let key = qualify_key(&tok.mods, &tok.name);
if !scope.contains(&key) {
return err(tok.span, format!("unbound variable '{key}'"));
}
Ok(MathElem::Embed {
expr: Ast::Var(scope.resolve(&key), tok.span),
span: tok.span,
})
}
c::MathBot::Sep(tok) => err(tok.0, "'|' builds a math list and may only be used when the math area starts with '|' (e.g. `${| a | b |}`); it cannot appear mid-formula or inside a `{ … }` math group"),
c::MathBot::Group { elems, .. } => Ok(MathElem::Group(lower_math_elems(elems, scope)?)),
}
}
fn math_group_arg<'s>(
g: &c::MathGroupArg,
scope: &Scope<'s>,
) -> Result<Vec<MathElem<'s>>, ElabError> {
match g {
c::MathGroupArg::Group { elems, .. } => lower_math_elems(elems, scope),
c::MathGroupArg::Bot(b) => Ok(vec![math_bot(b, scope)?]),
}
}
fn fold_math_scripts<'s>(
base: MathElem<'s>,
scripts: &[c::MathScript],
scope: &Scope<'s>,
) -> Result<MathElem<'s>, ElabError> {
let mut acc = base;
let mut i = 0;
while i < scripts.len() {
match &scripts[i] {
c::MathScript::Sub { group, .. } => {
if let Some(c::MathScript::Super { group: g2, .. }) = scripts.get(i + 1) {
let subg = math_group_arg(group, scope)?;
let supg = math_group_arg(g2, scope)?;
acc = MathElem::Sup(Box::new(MathElem::Sub(Box::new(acc), subg)), supg);
i += 2;
} else {
let subg = math_group_arg(group, scope)?;
acc = MathElem::Sub(Box::new(acc), subg);
i += 1;
}
}
c::MathScript::Super { group, .. } => {
if let Some(c::MathScript::Sub { group: g2, .. }) = scripts.get(i + 1) {
let supg = math_group_arg(group, scope)?;
let subg = math_group_arg(g2, scope)?;
acc = MathElem::Sup(Box::new(MathElem::Sub(Box::new(acc), subg)), supg);
i += 2;
} else {
let supg = math_group_arg(group, scope)?;
acc = MathElem::Sup(Box::new(acc), supg);
i += 1;
}
}
c::MathScript::Primes(tok) => {
acc = MathElem::Primes(Box::new(acc), tok.count);
i += 1;
}
}
}
Ok(acc)
}
fn math_arg_to_ast<'s>(arg: &c::MathArg, scope: &Scope<'s>) -> Result<Ast<'s>, ElabError> {
match arg {
c::MathArg::Plain(body) => math_arg_body_to_ast(body, scope),
c::MathArg::Optional { body, .. } => Ok(Ast::Ctor(
"Some".to_string(),
Some(Box::new(math_arg_body_to_ast(body, scope)?)),
)),
c::MathArg::Omission(_) => Ok(Ast::Ctor("None".to_string(), None)),
}
}
fn math_arg_body_to_ast<'s>(
body: &c::MathArgBody,
scope: &Scope<'s>,
) -> Result<Ast<'s>, ElabError> {
match body {
c::MathArgBody::Math { elems, .. } => math_block_ast(elems, scope),
c::MathArgBody::Inline { elems, .. } => inline_text_ast(elems, scope),
c::MathArgBody::Block { elems, .. } => {
Ok(Ast::BlockText(Rc::new(block_elems(elems, scope)?)))
}
c::MathArgBody::ParenEscape { inner, .. } => paren_body(inner, scope),
c::MathArgBody::ListEscape { items, .. } => {
let mut out = Vec::with_capacity(items.len());
for it in items {
out.push(expr(&it.value, scope)?);
}
Ok(Ast::List(out))
}
c::MathArgBody::RecordEscape { body, .. } => record_body_to_ast(body, scope),
}
}
fn omit_spaces(omit_pre: bool, omit_post: bool, raw: &str) -> String {
let s1 = if omit_pre {
omit_pre_spaces(raw)
} else {
raw.to_string()
};
let s2 = if omit_post { omit_post_spaces(&s1) } else { s1 };
let min_indent = min_indent_space(&s2);
let shaved = shave_indent(&s2, min_indent);
let mut chars: Vec<char> = shaved.chars().collect();
if chars.last() == Some(&'\n') {
chars.pop();
}
chars.into_iter().collect()
}
fn omit_pre_spaces(s: &str) -> String {
s.trim_start_matches(' ').to_string()
}
fn omit_post_spaces(s: &str) -> String {
let mut chars: Vec<char> = s.chars().collect();
loop {
match chars.last() {
Some(' ') => {
chars.pop();
}
Some('\n') => {
chars.pop();
break;
}
_ => break,
}
}
chars.into_iter().collect()
}
fn min_indent_space(s: &str) -> usize {
let chars: Vec<char> = s.chars().collect();
let mut reading_space = true;
let mut spnum = 0usize;
let mut minspnum = chars.len();
for ch in chars {
if reading_space {
match ch {
' ' => spnum += 1,
'\n' => spnum = 0,
_ => {
if spnum < minspnum {
minspnum = spnum;
}
reading_space = false;
}
}
} else if ch == '\n' {
reading_space = true;
spnum = 0;
}
}
minspnum
}
fn shave_indent(s: &str, minspnum: usize) -> String {
let mut out = String::new();
let mut reading_space = false;
let mut spnum = 0usize;
for ch in s.chars() {
if reading_space {
match ch {
' ' => {
if spnum >= minspnum {
out.push(' ');
}
spnum += 1;
}
'\n' => {
out.push('\n');
spnum = 0;
}
_ => {
out.push(ch);
reading_space = false;
}
}
} else if ch == '\n' {
out.push('\n');
reading_space = true;
spnum = 0;
} else {
out.push(ch);
}
}
out
}