pub mod ast;
pub(crate) mod compile;
pub mod crossref;
pub mod elaborate;
pub mod eval;
pub mod exhaustive;
pub mod hyphenation;
pub mod prim_types;
pub mod primitives;
pub mod quoted;
pub mod regexp;
pub mod symbol;
pub mod typecheck;
pub mod types;
pub mod unify;
pub mod v1;
pub mod value;
pub mod visit;
use crossref::{CrossRefs, Verdict};
use rustyfi_backend::{
place_block_at, placed_line_extent, shift_graphics, DecoId, FontMetrics, GraphicsElem, Length,
PureHorzBox, VertBox,
};
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use value::{DocumentValue, Value};
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error(transparent)]
Parse(#[from] rustyfi_syntax::ParseFileError),
#[error(transparent)]
Elaborate(#[from] elaborate::ElabError),
#[error(transparent)]
Type(#[from] typecheck::TypeError),
#[error(transparent)]
Eval(#[from] eval::EvalError),
#[error("the file's expression evaluated to {0}, not a document")]
NotADocument(&'static str),
#[error(transparent)]
Lower(#[from] v1::lower::LowerError),
#[error(
"cross-version import ({slice}): dependency {dep} references `{name}`, a \
version-forked builtin — {}",
v1::xver_adapt::forked_note(.name)
)]
CrossVersionUnsupportedName {
name: String,
dep: String,
slice: &'static str,
},
}
pub fn compile_document(
src: &str,
metrics: &dyn FontMetrics,
) -> Result<std::rc::Rc<DocumentValue>, CompileError> {
let file = rustyfi_syntax::parse_file(src)?;
compile_document_cst(&file, metrics)
}
pub fn compile_document_cst(
file: &rustyfi_syntax::cst::File,
metrics: &dyn FontMetrics,
) -> Result<std::rc::Rc<DocumentValue>, CompileError> {
compile_document_cst_with_trials(file, metrics).map(|(doc, _trials)| doc)
}
pub fn compile_document_cst_with_trials(
file: &rustyfi_syntax::cst::File,
metrics: &dyn FontMetrics,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
compile_document_cst_with_aux(file, metrics, &mut crossref::AuxTable::new())
}
pub fn compile_document_cst_with_aux(
file: &rustyfi_syntax::cst::File,
metrics: &dyn FontMetrics,
aux: &mut crossref::AuxTable,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
compile_document_cst_with_stages(file, metrics, aux, &std::collections::HashMap::new())
}
pub fn declared_stage(file: &rustyfi_syntax::cst::File) -> Option<types::Stage> {
use rustyfi_syntax::token::Token;
file.headers.iter().find_map(|h| match h {
rustyfi_syntax::cst::Header::Stage(st) => match st.tok {
Token::HeaderPersistent0 => Some(types::Stage::Persistent0),
Token::HeaderStage0 => Some(types::Stage::Stage0),
Token::HeaderStage1 => Some(types::Stage::Stage1),
_ => None,
},
_ => None,
})
}
fn note_stage(
stages: &mut std::collections::HashMap<usize, types::Stage>,
file: &rustyfi_syntax::cst::File,
start: usize,
end: usize,
) {
if let Some(stage) = declared_stage(file).filter(|s| *s != types::Stage::default()) {
stages.extend((start..end).map(|i| (i, stage)));
}
}
fn splice_staged(
prelude: &mut Vec<rustyfi_syntax::cst::TopBinding>,
stages: &mut std::collections::HashMap<usize, types::Stage>,
stage: Option<types::Stage>,
bindings: Vec<rustyfi_syntax::cst::TopBinding>,
) {
let start = prelude.len();
prelude.extend(bindings);
if let Some(st) = stage {
stages.extend((start..prelude.len()).map(|i| (i, st)));
}
}
fn splice_upgrade_glue(
prelude: &mut Vec<rustyfi_syntax::cst::TopBinding>,
stages: &mut std::collections::HashMap<usize, types::Stage>,
exports: &[(v1::xver_adapt::DecoExport, Option<types::Stage>)],
step: v1::xver_adapt::UpgradeStep,
) {
let mut order: Vec<Option<types::Stage>> = Vec::new();
for (_, st) in exports {
if !order.contains(st) {
order.push(*st);
}
}
for st in order {
let group: Vec<v1::xver_adapt::DecoExport> = exports
.iter()
.filter(|(_, s)| *s == st)
.map(|(e, _)| e.clone())
.collect();
splice_staged(
prelude,
stages,
st,
v1::xver_adapt::deco_upgrade_prelude(&group, step),
);
}
}
#[derive(Clone, Copy)]
struct Phase(Option<std::time::Instant>);
impl Phase {
fn start(timing: bool) -> Self {
Phase(timing.then(std::time::Instant::now))
}
fn ms(self) -> f64 {
self.0
.map(|t| t.elapsed().as_secs_f64() * 1e3)
.unwrap_or(0.0)
}
}
pub fn compile_document_cst_with_stages(
file: &rustyfi_syntax::cst::File,
metrics: &dyn FontMetrics,
aux: &mut crossref::AuxTable,
stages: &std::collections::HashMap<usize, types::Stage>,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
let timing = std::env::var_os("RUSTYFI_TIMING").is_some();
let t = Phase::start(timing);
let env0 = primitives::base_env();
let body = {
let store = symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env0.names());
let program = elaborate::elaborate_program_with_stages(file, &scope, stages)?;
if timing {
eprintln!("TIMING elaborate {:>8.1}ms", t.ms());
}
let t = Phase::start(timing);
typecheck::typecheck(&program)?;
if timing {
eprintln!("TIMING typecheck {:>8.1}ms", t.ms());
}
ast::debrand(&program.body, &store)
};
let t = Phase::start(timing);
let compiled = compile::compile_program(&body, &env0);
if timing {
eprintln!("TIMING compile-tree {:>8.1}ms", t.ms());
}
eval_document_trials(
&compiled,
metrics,
rustyfi_syntax::RustyfiVersion::V0_0,
aux,
)
}
pub fn check_document_cst_with_stages(
file: &rustyfi_syntax::cst::File,
stages: &std::collections::HashMap<usize, types::Stage>,
) -> Result<(), CompileError> {
let env0 = primitives::base_env();
let store = symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env0.names());
let program = elaborate::elaborate_program_with_stages(file, &scope, stages)?;
typecheck::typecheck(&program)?;
Ok(())
}
pub fn merge_v006_program(
program: rustyfi_loader::LoadedProgram,
) -> (
rustyfi_syntax::cst::File,
std::collections::HashMap<usize, types::Stage>,
) {
fn as_v006(cst: rustyfi_loader::LoadedCst) -> rustyfi_syntax::cst::File {
match cst {
rustyfi_loader::LoadedCst::V0_0(f) => f,
rustyfi_loader::LoadedCst::V0_1(_) => unreachable!(
"merge_v006_program is the V0_0-only path; a V0_1 file belongs \
on compile_document_v1's or compile_document_v006_xver's"
),
}
}
let mut files = program.files;
let entry = files.pop().expect("loader always yields the entry last");
let entry_cst = as_v006(entry.cst);
let mut prelude = Vec::new();
let mut stages = std::collections::HashMap::new();
for lib in files {
let mut cst = as_v006(lib.cst);
let start = prelude.len();
prelude.extend(std::mem::take(&mut cst.prelude));
note_stage(&mut stages, &cst, start, prelude.len());
}
prelude.extend(entry_cst.prelude);
(
rustyfi_syntax::cst::File {
headers: Vec::new(),
prelude,
in_kw: entry_cst.in_kw,
body: entry_cst.body,
eoi: entry_cst.eoi,
},
stages,
)
}
pub fn check_document_program(
program: rustyfi_loader::LoadedProgram,
version: rustyfi_syntax::RustyfiVersion,
) -> Result<(), CompileError> {
match version {
rustyfi_syntax::RustyfiVersion::V0_1 => check_document_v1(&program.files),
_ => {
let has_v01_dep = program
.files
.iter()
.any(|f| matches!(f.cst, rustyfi_loader::LoadedCst::V0_1(_)));
if has_v01_dep {
check_document_v006_xver(&program.files)
} else {
let (merged, stages) = merge_v006_program(program);
check_document_cst_with_stages(&merged, &stages)
}
}
}
}
fn eval_document_trials(
compiled: &compile::CompiledExpr,
metrics: &dyn FontMetrics,
version: rustyfi_syntax::RustyfiVersion,
aux: &mut crossref::AuxTable,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
if !aux.is_empty() {
let (doc, trials, table, unvalidated) =
eval_trials_seeded(compiled, metrics, version, aux.clone())?;
if !unvalidated {
*aux = table;
return Ok((doc, trials));
}
}
let (doc, trials, table, _) =
eval_trials_seeded(compiled, metrics, version, crossref::AuxTable::new())?;
*aux = table;
Ok((doc, trials))
}
fn eval_trials_seeded(
compiled: &compile::CompiledExpr,
metrics: &dyn FontMetrics,
version: rustyfi_syntax::RustyfiVersion,
seed: crossref::AuxTable,
) -> Result<(std::rc::Rc<DocumentValue>, u32, crossref::AuxTable, bool), CompileError> {
let timing = std::env::var_os("RUSTYFI_TIMING").is_some();
let crossrefs = Rc::new(RefCell::new(CrossRefs::seeded(seed)));
let mut trials = 0u32;
loop {
trials += 1;
let t_trial = Phase::start(timing);
let env = value::Env::root();
let mut interp = eval::Interp::new(metrics);
interp.crossrefs = crossrefs.clone();
interp.version = version;
let doc = match compiled.run(&env, &mut interp)? {
Value::Document(doc) => doc,
other => return Err(CompileError::NotADocument(other.type_name())),
};
let t_hooks = Phase::start(timing);
let run_ms = t_trial.ms();
fire_hooks(&mut interp, &doc)?;
if timing {
eprintln!(
"TIMING trial {trials}: run(eval+layout) {:>8.1}ms fire_hooks {:>6.1}ms",
run_ms,
t_hooks.ms()
);
}
let verdict = crossrefs.borrow_mut().verdict();
match verdict {
Verdict::NeedsAnotherTrial => continue,
Verdict::CanTerminate(_) | Verdict::CountMax => {
let mut final_doc = Rc::try_unwrap(doc).unwrap_or_else(|rc| (*rc).clone());
final_doc.extras = rustyfi_backend::DocExtras {
annotations: std::mem::take(&mut interp.annotations),
destinations: std::mem::take(&mut interp.destinations),
outline: std::mem::take(&mut interp.outline),
page_graphics: std::mem::take(&mut interp.page_graphics),
doc_info: interp.doc_info.take(),
};
final_doc.reflow_links = std::mem::take(&mut interp.link_decos);
final_doc.reflow_dests = std::mem::take(&mut interp.dest_decos);
final_doc.reflow_frame_decos = std::mem::take(&mut interp.frame_decos);
let refs = crossrefs.borrow();
return Ok((
Rc::new(final_doc),
trials,
refs.export(),
refs.seed_unvalidated(),
));
}
}
}
}
pub fn compile_document_v1(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
) -> Result<std::rc::Rc<DocumentValue>, CompileError> {
compile_document_v1_with_trials(files, metrics).map(|(doc, _trials)| doc)
}
pub fn compile_document_v1_with_trials(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
compile_document_v1_with_aux(files, metrics, &mut crossref::AuxTable::new())
}
pub fn compile_document_v1_with_aux(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
aux: &mut crossref::AuxTable,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
use rustyfi_syntax::RustyfiVersion;
let asm = assemble_v1(files)?;
let env0 = primitives::base_env_with_version(RustyfiVersion::V0_1);
let body = {
let store = symbol::SymbolStore::new();
let program = check_v1(&asm, &store, &env0)?;
ast::debrand(&program.body, &store)
};
let compiled = if asm.v006_indices.is_empty() {
compile::compile_program(&body, &env0)
} else {
let env0_v006 = primitives::base_env_with_version(RustyfiVersion::V0_0);
compile::compile_program_xver(&body, &env0, &env0_v006)
};
eval_document_trials(&compiled, metrics, RustyfiVersion::V0_1, aux)
}
pub fn check_document_v1(files: &[rustyfi_loader::LoadedFile]) -> Result<(), CompileError> {
let asm = assemble_v1(files)?;
let env0 = primitives::base_env_with_version(rustyfi_syntax::RustyfiVersion::V0_1);
let store = symbol::SymbolStore::new();
check_v1(&asm, &store, &env0).map(|_program| ())
}
struct AssembledV1<'a> {
file: rustyfi_syntax::cst::File,
dep_csts: Vec<&'a rustyfi_syntax::cst_v1::FileV1>,
v006_indices: std::collections::HashSet<usize>,
stages: std::collections::HashMap<usize, types::Stage>,
}
fn check_v1<'s>(
asm: &AssembledV1<'_>,
store: &'s symbol::SymbolStore,
env0: &value::BaseEnv,
) -> Result<elaborate::Program<'s>, CompileError> {
use rustyfi_syntax::RustyfiVersion;
let scope_names: Vec<String> = if asm.v006_indices.is_empty() {
env0.names()
} else {
let mut n = env0.names();
n.extend(primitives::base_env_with_version(RustyfiVersion::V0_0).names());
n.sort();
n.dedup();
n
};
let scope = elaborate::Scope::new_with_version(store, scope_names, RustyfiVersion::V0_1);
let program = elaborate::elaborate_program_with_versions(
&asm.file,
&scope,
&asm.v006_indices,
&asm.stages,
None,
)?;
v1::module_check::check_program(&asm.dep_csts, &program)?;
Ok(program)
}
fn assemble_v1<'a>(
files: &'a [rustyfi_loader::LoadedFile],
) -> Result<AssembledV1<'a>, CompileError> {
use rustyfi_syntax::RustyfiVersion;
let (entry, deps) = files
.split_last()
.expect("loader always yields at least the entry file");
fn as_v01(f: &rustyfi_loader::LoadedFile) -> &rustyfi_syntax::cst_v1::FileV1 {
match &f.cst {
rustyfi_loader::LoadedCst::V0_1(cst) => cst,
rustyfi_loader::LoadedCst::V0_0(_) => unreachable!(
"as_v01 called on a V0_0-parsed file — the entry is always \
V0_1 under compile_document_v1, and every V0_0 dependency \
is routed through the X1 cross-version splice arm instead"
),
}
}
let mut surfaces = v1::surface::SurfaceEnv::default();
let mut prelude = Vec::new();
let mut dep_csts: Vec<&rustyfi_syntax::cst_v1::FileV1> = Vec::new();
let mut v006_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut stages: std::collections::HashMap<usize, types::Stage> =
std::collections::HashMap::new();
let mut deco_exports: Vec<(v1::xver_adapt::DecoExport, Option<types::Stage>)> = Vec::new();
let mut v006_view_installed = false;
let mut deco_view_captured: usize = 0;
for dep in deps {
match &dep.cst {
rustyfi_loader::LoadedCst::V0_1(cst) => {
if v006_view_installed {
splice_upgrade_glue(
&mut prelude,
&mut stages,
&deco_exports[..deco_view_captured],
v1::xver_adapt::UpgradeStep::Install,
);
v006_view_installed = false;
}
v1::surface::build_file_surface(cst, &mut surfaces);
prelude.extend(v1::lower::lower_file_v1_with_surfaces(cst, &surfaces)?);
dep_csts.push(cst);
}
rustyfi_loader::LoadedCst::V0_0(cst) => {
if !v006_view_installed || deco_view_captured < deco_exports.len() {
if deco_view_captured < deco_exports.len() {
splice_upgrade_glue(
&mut prelude,
&mut stages,
&deco_exports[deco_view_captured..],
v1::xver_adapt::UpgradeStep::Capture,
);
deco_view_captured = deco_exports.len();
}
splice_upgrade_glue(
&mut prelude,
&mut stages,
&deco_exports,
v1::xver_adapt::UpgradeStep::Restore,
);
v006_view_installed = !deco_exports.is_empty();
}
let free = collect_free_globals(&cst.prelude);
let reject_t = v1::xver_adapt::reject_type_names_from_v006();
let touched: std::collections::BTreeSet<String> =
free.types.intersection(&reject_t).cloned().collect();
if let Some(name) = touched
.iter()
.find(|n| !matches!(n.as_str(), "math" | "deco" | "deco-set" | "paren"))
{
return Err(CompileError::CrossVersionUnsupportedName {
name: name.clone(),
dep: dep.path.display().to_string(),
slice: "X3",
});
}
if touched.contains("deco")
|| touched.contains("deco-set")
|| touched.contains("paren")
{
let probe = v1::xver_adapt::classify_deco_exports(
&cst.prelude,
RustyfiVersion::V0_0,
RustyfiVersion::V0_1,
);
if probe
.as_ref()
.map(|e| v1::xver_adapt::needs_unite_helper(e))
== Ok(true)
{
let helper_start = prelude.len();
prelude.extend(v1::xver_adapt::unite_helper_prelude());
stages.extend(
(helper_start..prelude.len())
.map(|i| (i, types::Stage::Persistent0)),
);
}
}
let start = prelude.len();
if touched.is_empty() {
prelude.extend(cst.prelude.iter().cloned());
} else if touched.contains("math") {
let adapted = v1::xver_adapt::relabel_type_decls(
&cst.prelude,
RustyfiVersion::V0_0,
RustyfiVersion::V0_1,
)
.map_err(|be| {
CompileError::CrossVersionUnsupportedName {
name: match &be {
v1::xver_adapt::BoundaryError::ForkedTypeExport {
ty_name, ..
} => ty_name.clone(),
},
dep: dep.path.display().to_string(),
slice: "X3",
}
})?;
prelude.extend(adapted);
} else {
prelude.extend(cst.prelude.iter().cloned());
}
v006_indices.extend(start..prelude.len());
note_stage(&mut stages, cst, start, prelude.len());
if touched.contains("deco")
|| touched.contains("deco-set")
|| touched.contains("paren")
{
let exports = v1::xver_adapt::classify_deco_exports(
&cst.prelude,
RustyfiVersion::V0_0,
RustyfiVersion::V0_1,
)
.map_err(|be| {
CompileError::CrossVersionUnsupportedName {
name: match &be {
v1::xver_adapt::BoundaryError::ForkedTypeExport {
ty_name, ..
} => ty_name.clone(),
},
dep: dep.path.display().to_string(),
slice: "X3b",
}
})?;
v1::xver_adapt::inject_module_deco_wrappers(&mut prelude[start..], &exports);
let dep_stage =
declared_stage(cst).filter(|s| *s != types::Stage::default());
splice_staged(
&mut prelude,
&mut stages,
dep_stage,
v1::xver_adapt::deco_coercion_prelude(&exports),
);
deco_exports.extend(exports.into_iter().map(|e| (e, dep_stage)));
}
}
}
}
if v006_view_installed {
splice_upgrade_glue(
&mut prelude,
&mut stages,
&deco_exports[..deco_view_captured],
v1::xver_adapt::UpgradeStep::Install,
);
}
let entry_cst = as_v01(entry);
let body = v1::lower::lower_document_v1(entry_cst)?;
let eoi = match entry_cst {
rustyfi_syntax::cst_v1::FileV1::Document { eoi, .. } => eoi.clone(),
_ => unreachable!("lower_document_v1 already rejected a Library entry"),
};
let file = rustyfi_syntax::cst::File {
headers: Vec::new(),
prelude,
in_kw: Some(rustyfi_syntax::leaf::KwIn(rustyfi_syntax::Span::default())),
body: Some(body),
eoi,
};
Ok(AssembledV1 {
file,
dep_csts,
v006_indices,
stages,
})
}
pub fn compile_document_v006_xver(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
) -> Result<std::rc::Rc<DocumentValue>, CompileError> {
compile_document_v006_xver_with_trials(files, metrics).map(|(doc, _trials)| doc)
}
pub fn compile_document_v006_xver_with_trials(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
compile_document_v006_xver_with_aux(files, metrics, &mut crossref::AuxTable::new())
}
pub fn compile_document_v006_xver_with_aux(
files: &[rustyfi_loader::LoadedFile],
metrics: &dyn FontMetrics,
aux: &mut crossref::AuxTable,
) -> Result<(std::rc::Rc<DocumentValue>, u32), CompileError> {
use rustyfi_syntax::RustyfiVersion;
let asm = assemble_v006_xver(files)?;
let env0 = primitives::base_env_with_version(RustyfiVersion::V0_1);
let store = symbol::SymbolStore::new();
let program = check_v006_xver(&asm, &store, &env0)?;
let env0_v006 = primitives::base_env_with_version(RustyfiVersion::V0_0);
let body = ast::debrand(&program.body, &store);
let compiled = compile::compile_program_xver(&body, &env0, &env0_v006);
eval_document_trials(&compiled, metrics, RustyfiVersion::V0_0, aux)
}
pub fn check_document_v006_xver(files: &[rustyfi_loader::LoadedFile]) -> Result<(), CompileError> {
let asm = assemble_v006_xver(files)?;
let env0 = primitives::base_env_with_version(rustyfi_syntax::RustyfiVersion::V0_1);
let store = symbol::SymbolStore::new();
check_v006_xver(&asm, &store, &env0).map(|_program| ())
}
struct AssembledXver<'a> {
file: rustyfi_syntax::cst::File,
dep_csts: Vec<&'a rustyfi_syntax::cst_v1::FileV1>,
v006_indices: std::collections::HashSet<usize>,
stages: std::collections::HashMap<usize, types::Stage>,
xver_shadows: std::collections::HashSet<String>,
}
fn check_v006_xver<'s>(
asm: &AssembledXver<'_>,
store: &'s symbol::SymbolStore,
env0: &value::BaseEnv,
) -> Result<elaborate::Program<'s>, CompileError> {
use rustyfi_syntax::RustyfiVersion;
let scope = elaborate::Scope::new_with_version(store, env0.names(), RustyfiVersion::V0_1);
let program = elaborate::elaborate_program_with_versions(
&asm.file,
&scope,
&asm.v006_indices,
&asm.stages,
Some(RustyfiVersion::V0_0),
)?;
v1::module_check::check_program_with_xver_shadows(
&asm.dep_csts,
&program,
&asm.xver_shadows,
)?;
Ok(program)
}
fn assemble_v006_xver<'a>(
files: &'a [rustyfi_loader::LoadedFile],
) -> Result<AssembledXver<'a>, CompileError> {
let (entry_idx, entry) = files
.iter()
.enumerate()
.find(|(_, f)| f.cst.is_document())
.expect("loader validated exactly one document (the entry)");
let entry_cst = match &entry.cst {
rustyfi_loader::LoadedCst::V0_0(f) => f,
rustyfi_loader::LoadedCst::V0_1(_) => unreachable!(
"compile_document_v006_xver is the V0_0-entry sibling of \
compile_document_v1 — a V0_1 entry belongs there instead"
),
};
let mut surfaces = v1::surface::SurfaceEnv::default();
let mut prelude = Vec::new();
let mut dep_csts: Vec<&rustyfi_syntax::cst_v1::FileV1> = Vec::new();
let mut v006_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut stages: std::collections::HashMap<usize, types::Stage> =
std::collections::HashMap::new();
let mut xver_shadows: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut deco_exports: Vec<v1::xver_adapt::DecoExport> = Vec::new();
let mut v006_view_installed = false;
for (i, dep) in files.iter().enumerate() {
if i == entry_idx {
continue;
}
match &dep.cst {
rustyfi_loader::LoadedCst::V0_0(cst) => {
let adapted = guard_v006_type_text(&cst.prelude, &dep.path)?;
if !v006_view_installed && !deco_exports.is_empty() {
prelude.extend(v1::xver_adapt::deco_downgrade_prelude(
&deco_exports,
v1::xver_adapt::DowngradeStep::Install,
));
v006_view_installed = true;
}
let start = prelude.len();
prelude.extend(adapted);
v006_indices.extend(start..prelude.len());
note_stage(&mut stages, cst, start, prelude.len());
}
rustyfi_loader::LoadedCst::V0_1(cst) => {
if v006_view_installed {
prelude.extend(v1::xver_adapt::deco_downgrade_prelude(
&deco_exports,
v1::xver_adapt::DowngradeStep::Restore,
));
v006_view_installed = false;
}
v1::surface::build_file_surface(cst, &mut surfaces);
let lowered = v1::lower::lower_file_v1_with_surfaces(cst, &surfaces)?;
let free = collect_free_globals(&lowered);
let reject_t = v1::xver_adapt::reject_type_names();
let touched: BTreeSet<String> =
free.types.intersection(&reject_t).cloned().collect();
if let Some(name) = touched.iter().find(|n| {
!matches!(n.as_str(), "math-text" | "math-boxes" | "deco" | "deco-set")
}) {
return Err(CompileError::CrossVersionUnsupportedName {
name: name.clone(),
dep: dep.path.display().to_string(),
slice: "X4a",
});
}
let dep_deco_exports = v1::xver_adapt::classify_deco_exports_v01_sig(
cst, &surfaces,
)
.map_err(|be| CompileError::CrossVersionUnsupportedName {
name: match &be {
v1::xver_adapt::BoundaryError::ForkedTypeExport { ty_name, .. } => {
ty_name.clone()
}
},
dep: dep.path.display().to_string(),
slice: "X4b",
})?;
prelude.extend(lowered);
prelude.extend(v1::xver_adapt::deco_downgrade_prelude(
&dep_deco_exports,
v1::xver_adapt::DowngradeStep::Capture,
));
for exp in &dep_deco_exports {
xver_shadows.insert(v1::xver_adapt::deco_export_qualified_name(exp));
}
deco_exports.extend(dep_deco_exports);
dep_csts.push(cst);
}
}
}
if !v006_view_installed && !deco_exports.is_empty() {
prelude.extend(v1::xver_adapt::deco_downgrade_prelude(
&deco_exports,
v1::xver_adapt::DowngradeStep::Install,
));
}
let entry_adapted = guard_v006_type_text(&entry_cst.prelude, &entry.path)?;
let entry_start = prelude.len();
prelude.extend(entry_adapted);
v006_indices.extend(entry_start..prelude.len());
let file = rustyfi_syntax::cst::File {
headers: Vec::new(),
prelude,
in_kw: entry_cst.in_kw.clone(),
body: entry_cst.body.clone(),
eoi: entry_cst.eoi.clone(),
};
Ok(AssembledXver {
file,
dep_csts,
v006_indices,
stages,
xver_shadows,
})
}
#[derive(Default, Debug)]
struct FreeGlobals {
values: BTreeSet<String>,
types: BTreeSet<String>,
}
#[derive(Default)]
struct XverScope {
values: Vec<String>,
types: Vec<String>,
}
impl XverScope {
fn mark(&self) -> (usize, usize) {
(self.values.len(), self.types.len())
}
fn truncate_to(&mut self, mark: (usize, usize)) {
self.values.truncate(mark.0);
self.types.truncate(mark.1);
}
fn push_value(&mut self, name: &str) {
self.values.push(name.to_string());
}
fn push_type(&mut self, name: &str) {
self.types.push(name.to_string());
}
fn has_value(&self, name: &str) -> bool {
self.values.iter().any(|v| v == name)
}
fn has_type(&self, name: &str) -> bool {
self.types.iter().any(|v| v == name)
}
}
fn emit_value(scope: &XverScope, out: &mut FreeGlobals, name: &str) {
if !scope.has_value(name) {
out.values.insert(name.to_string());
}
}
fn emit_type(scope: &XverScope, out: &mut FreeGlobals, name: &str) {
if !scope.has_type(name) {
out.types.insert(name.to_string());
}
}
fn collect_free_globals(prelude: &[rustyfi_syntax::cst::TopBinding]) -> FreeGlobals {
let mut out = FreeGlobals::default();
let mut scope = XverScope::default();
for tb in prelude {
walk_top_binding(tb, &mut scope, &mut out);
}
out
}
fn walk_top_binding(
tb: &rustyfi_syntax::cst::TopBinding,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::TopBinding;
match tb {
TopBinding::LetRec { first, ands, .. } => {
scope.push_value(&first.name.name);
for and in ands {
scope.push_value(&and.binding.name.name);
}
walk_rec_binding_body(first, true, scope, out);
for and in ands {
walk_rec_binding_body(&and.binding, true, scope, out);
}
}
TopBinding::Let(tl) => {
if let Some(asc) = &tl.ascription {
walk_type_expr(&asc.ty, scope, out);
}
let mark = scope.mark();
for p in &tl.params {
walk_param_binder(p, scope, out);
}
walk_expr(&tl.value, scope, out);
scope.truncate_to(mark);
scope.push_value(&tl.name.name);
}
TopBinding::LetPattern { value, .. } => {
walk_expr(value, scope, out);
}
TopBinding::LetInline {
ctx,
cmd,
params,
value,
..
} => {
let mark = scope.mark();
if let Some(c) = ctx {
scope.push_value(&c.name);
}
for p in params {
walk_param_binder(p, scope, out);
}
walk_expr(value, scope, out);
scope.truncate_to(mark);
scope.push_value(&cmd.name);
}
TopBinding::LetBlock {
ctx,
cmd,
params,
value,
..
} => {
let mark = scope.mark();
if let Some(c) = ctx {
scope.push_value(&c.name);
}
for p in params {
walk_param_binder(p, scope, out);
}
walk_expr(value, scope, out);
scope.truncate_to(mark);
scope.push_value(&cmd.name);
}
TopBinding::LetMath {
cmd, params, value, ..
} => {
let mark = scope.mark();
for p in params {
walk_param_binder(p, scope, out);
}
walk_expr(value, scope, out);
scope.truncate_to(mark);
scope.push_value(&cmd.name);
}
TopBinding::Type(td) => {
walk_type_decl(td, scope, out);
scope.push_type(&td.name.name);
}
TopBinding::LetMutable { name, value, .. } => {
walk_expr(value, scope, out);
scope.push_value(&name.name);
}
TopBinding::Module { sig, decls, .. } => {
if let Some(sig) = sig {
walk_sig_annot(sig, scope, out);
}
let mark = scope.mark();
for d in decls {
walk_top_binding(&d.0, scope, out);
}
scope.truncate_to(mark);
}
TopBinding::Open { .. } => {}
}
}
fn walk_rec_binding_body(
rb: &rustyfi_syntax::cst::ast::RecBinding,
boundary: bool,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
if boundary {
if let Some(asc) = &rb.ascription {
walk_type_expr(&asc.ty, scope, out);
}
}
let mark = scope.mark();
for p in &rb.params {
walk_patbot_binder(p, scope, out);
}
walk_expr(&rb.value.0, scope, out);
scope.truncate_to(mark);
for clause in &rb.extra {
let mark = scope.mark();
for p in &clause.params {
walk_patbot_binder(p, scope, out);
}
walk_expr(&clause.value.0, scope, out);
scope.truncate_to(mark);
}
}
fn walk_param_binder(
p: &rustyfi_syntax::cst::ast::Param,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::Param;
match p {
Param::Optional { name, .. } => scope.push_value(&name.name),
Param::Pat(pb) => walk_patbot_binder(pb, scope, out),
Param::Bundled { opts, body } => {
for e in &opts.entries {
scope.push_value(&e.var.name);
}
walk_patbot_binder(body, scope, out);
}
}
}
fn walk_pattern_binder(
pat: &rustyfi_syntax::cst::ast::Pattern,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_patcons_binder(&pat.head, scope, out);
if let Some(ac) = &pat.as_clause {
scope.push_value(&ac.name.name);
}
}
fn walk_patcons_binder(
pc: &rustyfi_syntax::cst::ast::PatCons,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_patbot_binder(&pc.head, scope, out);
for seg in &pc.tail {
walk_patbot_binder(&seg.tail, scope, out);
}
}
fn walk_patbot_binder(
pb: &rustyfi_syntax::cst::ast::PatBot,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::PatBot;
match pb {
PatBot::CtorApplied { ctor, arg } => {
emit_value(scope, out, &ctor.name);
walk_patbot_binder(arg, scope, out);
}
PatBot::Ctor(ctor) => emit_value(scope, out, &ctor.name),
PatBot::Int(_) | PatBot::True(_) | PatBot::False(_) | PatBot::Str(_) | PatBot::Wild(_) => {}
PatBot::Var(v) => scope.push_value(&v.name),
PatBot::Unit { .. } => {}
PatBot::Paren { inner, .. } => {
walk_pattern_binder(&inner.first.0, scope, out);
for cp in &inner.rest {
walk_pattern_binder(&cp.value.0, scope, out);
}
}
PatBot::List { items, .. } => {
for it in items {
walk_pattern_binder(&it.value.0, scope, out);
}
}
}
}
fn collect_type_decl_globals(
prelude: &[rustyfi_syntax::cst::TopBinding],
) -> std::collections::BTreeSet<String> {
let mut out = FreeGlobals::default();
let mut scope = XverScope::default();
for tb in prelude {
walk_type_decls_only(tb, &mut scope, &mut out);
}
out.types
}
fn walk_type_decls_only(
tb: &rustyfi_syntax::cst::TopBinding,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::TopBinding;
match tb {
TopBinding::Type(td) => {
walk_type_decl(td, scope, out);
scope.push_type(&td.name.name);
}
TopBinding::Module { decls, .. } => {
let mark = scope.mark();
for d in decls {
walk_type_decls_only(&d.0, scope, out);
}
scope.truncate_to(mark);
}
_ => {}
}
}
fn guard_v006_type_text(
prelude: &[rustyfi_syntax::cst::TopBinding],
path: &std::path::Path,
) -> Result<Vec<rustyfi_syntax::cst::TopBinding>, CompileError> {
use rustyfi_syntax::RustyfiVersion;
let reject_t = v1::xver_adapt::reject_type_names_from_v006();
let touched: BTreeSet<String> = collect_type_decl_globals(prelude)
.intersection(&reject_t)
.cloned()
.collect();
if let Some(name) = touched.iter().find(|n| n.as_str() != "math") {
return Err(CompileError::CrossVersionUnsupportedName {
name: name.clone(),
dep: path.display().to_string(),
slice: "X4c",
});
}
if touched.is_empty() {
return Ok(prelude.to_vec());
}
v1::xver_adapt::relabel_type_decls(prelude, RustyfiVersion::V0_0, RustyfiVersion::V0_1).map_err(
|be| CompileError::CrossVersionUnsupportedName {
name: match &be {
v1::xver_adapt::BoundaryError::ForkedTypeExport { ty_name, .. } => ty_name.clone(),
},
dep: path.display().to_string(),
slice: "X4c",
},
)
}
fn walk_type_decl(
td: &rustyfi_syntax::cst::TypeDecl,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_type_decl_body(&td.body, scope, out);
for a in &td.ands {
walk_type_decl_body(&a.body, scope, out);
}
}
fn walk_type_decl_body(
body: &rustyfi_syntax::cst::TypeDeclBody,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::TypeDeclBody;
match body {
TypeDeclBody::Variant { first, rest, .. } => {
walk_variant_def(first, scope, out);
for bv in rest {
walk_variant_def(&bv.def, scope, out);
}
}
TypeDeclBody::Synonym(ty) => walk_type_expr(ty, scope, out),
}
}
fn walk_variant_def(
vd: &rustyfi_syntax::cst::VariantDef,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
if let Some(of_ty) = &vd.of_ty {
walk_type_expr(&of_ty.ty, scope, out);
}
}
fn walk_sig_annot(
sig: &rustyfi_syntax::cst::SigAnnot,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::SigItem;
for item in &sig.items {
match item {
SigItem::ValHorzCmd { ty, .. }
| SigItem::ValVertCmd { ty, .. }
| SigItem::Val { ty, .. }
| SigItem::DirectHorzCmd { ty, .. }
| SigItem::DirectVertCmd { ty, .. } => walk_type_expr(ty, scope, out),
SigItem::Type { .. } => {}
}
}
}
fn walk_expr(e: &rustyfi_syntax::cst::ast::Expr, scope: &mut XverScope, out: &mut FreeGlobals) {
use rustyfi_syntax::cst::ast::Expr;
match e {
Expr::LetRecIn {
first, ands, body, ..
} => {
let mark = scope.mark();
scope.push_value(&first.name.name);
for and in ands {
scope.push_value(&and.binding.name.name);
}
walk_rec_binding_body(first, false, scope, out);
for and in ands {
walk_rec_binding_body(&and.binding, false, scope, out);
}
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::LetIn {
name,
params,
value,
body,
..
} => {
let mark = scope.mark();
for p in params {
walk_param_binder(p, scope, out);
}
walk_expr(value, scope, out);
scope.truncate_to(mark);
let mark = scope.mark();
scope.push_value(&name.name);
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::LetPatternIn {
pat, value, body, ..
} => {
walk_expr(value, scope, out);
let mark = scope.mark();
walk_pattern_binder(&pat.0, scope, out);
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::If {
cond,
then_branch,
else_branch,
..
} => {
walk_expr(cond, scope, out);
walk_expr(then_branch, scope, out);
walk_expr(else_branch, scope, out);
}
Expr::Fun { params, body, .. } => {
let mark = scope.mark();
for p in params {
walk_patbot_binder(p, scope, out);
}
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::FunRows {
opts, param, body, ..
} => {
let mark = scope.mark();
for e in &opts.entries {
scope.push_value(&e.var.name);
}
walk_patbot_binder(param, scope, out);
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::Match {
scrutinee,
first,
rest,
..
} => {
walk_expr(scrutinee, scope, out);
walk_match_arm(first, scope, out);
for ba in rest {
walk_match_arm(&ba.arm, scope, out);
}
}
Expr::LetMutableIn {
name, init, body, ..
} => {
walk_expr(init, scope, out);
let mark = scope.mark();
scope.push_value(&name.name);
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::LetMathIn {
cmd,
params,
value,
body,
..
} => {
let mark = scope.mark();
for p in params {
walk_param_binder(p, scope, out);
}
walk_expr(value, scope, out);
scope.truncate_to(mark);
let mark = scope.mark();
scope.push_value(&cmd.name);
walk_expr(body, scope, out);
scope.truncate_to(mark);
}
Expr::OpenIn { body, .. } => walk_expr(body, scope, out),
Expr::WhileDo { cond, body, .. } => {
walk_expr(cond, scope, out);
walk_expr(body, scope, out);
}
Expr::Overwrite { name, value, .. } => {
emit_value(scope, out, &name.name);
walk_expr(&value.0, scope, out);
}
Expr::Ops(chain) => walk_opchain(chain, scope, out),
}
}
fn walk_match_arm(
arm: &rustyfi_syntax::cst::ast::MatchArm,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
let mark = scope.mark();
walk_pattern_binder(&arm.pat.0, scope, out);
if let Some(g) = &arm.guard {
walk_expr(&g.cond.0, scope, out);
}
walk_expr(&arm.body.0, scope, out);
scope.truncate_to(mark);
}
fn walk_opchain(
oc: &rustyfi_syntax::cst::ast::OpChain,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_appexpr(&oc.head, scope, out);
for r in &oc.tail {
walk_appexpr(&r.rhs, scope, out);
}
if let Some(bt) = &oc.before {
walk_expr(&bt.body.0, scope, out);
}
}
fn walk_appexpr(
ae: &rustyfi_syntax::cst::ast::AppExpr,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_atomic(&ae.head, scope, out);
for arg in &ae.args {
walk_apparg(arg, scope, out);
}
}
fn walk_apparg(a: &rustyfi_syntax::cst::ast::AppArg, scope: &mut XverScope, out: &mut FreeGlobals) {
use rustyfi_syntax::cst::ast::AppArg;
match a {
AppArg::Optional { value, .. } => walk_atomic(value, scope, out),
AppArg::Omission(_) => {}
AppArg::Atom { atom, .. } => walk_atomic(atom, scope, out),
AppArg::Ctor(c) => emit_value(scope, out, &c.name),
AppArg::Bundled { opts, atom, .. } => {
for e in &opts.entries {
walk_expr(&e.value.0, scope, out);
}
walk_atomic(atom, scope, out);
}
AppArg::BundledCtor { opts, ctor } => {
for e in &opts.entries {
walk_expr(&e.value.0, scope, out);
}
emit_value(scope, out, &ctor.name);
}
}
}
fn walk_atomic(a: &rustyfi_syntax::cst::ast::Atomic, scope: &mut XverScope, out: &mut FreeGlobals) {
use rustyfi_syntax::cst::ast::Atomic;
match a {
Atomic::Length(_)
| Atomic::Float(_)
| Atomic::Int(_)
| Atomic::Literal(_)
| Atomic::True(_)
| Atomic::False(_) => {}
Atomic::Ctor(c) => emit_value(scope, out, &c.name),
Atomic::Var(v) => emit_value(scope, out, &v.name),
Atomic::VarWithMod(_) => {}
Atomic::OpRef(op) => emit_value(scope, out, &op.name),
Atomic::Command { name, .. } => walk_any_horz_cmd_ref(name, scope, out),
Atomic::Unit { .. } => {}
Atomic::Paren { inner, .. } => walk_paren_body(inner, scope, out),
Atomic::OpenModule { body, .. } => walk_paren_body(body, scope, out),
Atomic::Record { body, .. } => walk_record_body(body, scope, out),
Atomic::List { items, .. } => {
for it in items {
walk_expr(&it.value.0, scope, out);
}
}
Atomic::InlineText { elems, .. } => {
for el in elems {
walk_inline_elem(el, scope, out);
}
}
Atomic::BlockText { elems, .. } => {
for el in elems {
walk_block_elem(el, scope, out);
}
}
Atomic::MathText { elems, .. } => {
for el in elems {
walk_math_elem(&el.0, scope, out);
}
}
}
}
fn walk_any_horz_cmd_ref(
n: &rustyfi_syntax::leaf::AnyHorzCmdTok,
scope: &XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::leaf::AnyHorzCmdTok;
match n {
AnyHorzCmdTok::Plain(t) => emit_value(scope, out, &t.name),
AnyHorzCmdTok::Mod(_) => {} }
}
fn walk_any_vert_cmd_ref(
n: &rustyfi_syntax::leaf::AnyVertCmdTok,
scope: &XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::leaf::AnyVertCmdTok;
match n {
AnyVertCmdTok::Plain(t) => emit_value(scope, out, &t.name),
AnyVertCmdTok::Mod(_) => {} }
}
fn walk_any_math_cmd_ref(
n: &rustyfi_syntax::leaf::AnyMathCmdTok,
scope: &XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::leaf::AnyMathCmdTok;
match n {
AnyMathCmdTok::Plain(t) => emit_value(scope, out, &t.name),
AnyMathCmdTok::Mod(_) => {} }
}
fn walk_paren_body(
pb: &rustyfi_syntax::cst::ast::ParenBody,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_expr(&pb.first.0, scope, out);
for ce in &pb.rest {
walk_expr(&ce.value.0, scope, out);
}
}
fn walk_record_body(
rb: &rustyfi_syntax::cst::ast::RecordBody,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::RecordBody;
match rb {
RecordBody::Update { base, fields, .. } => {
walk_expr(&base.0, scope, out);
for f in fields {
walk_expr(&f.value.0, scope, out);
}
}
RecordBody::Fields(fields) => {
for f in fields {
walk_expr(&f.value.0, scope, out);
}
}
}
}
fn walk_inline_elem(
el: &rustyfi_syntax::cst::ast::InlineElem,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::InlineElem;
match el {
InlineElem::Char(_)
| InlineElem::CodeText(_)
| InlineElem::Space(_)
| InlineElem::Break(_) => {}
InlineElem::Embed { var, .. } => {
if var.mods.is_empty() {
emit_value(scope, out, &var.name);
}
}
InlineElem::EmbedMath { elems, .. } => {
for m in elems {
walk_math_elem(&m.0, scope, out);
}
}
InlineElem::Cmd { name, tail } => {
walk_any_horz_cmd_ref(name, scope, out);
walk_cmd_tail(tail, scope, out);
}
InlineElem::ItemBullet(_) | InlineElem::Sep(_) => {}
}
}
fn walk_block_elem(
el: &rustyfi_syntax::cst::ast::BlockElem,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::BlockElem;
match el {
BlockElem::Embed { var, .. } => {
if var.mods.is_empty() {
emit_value(scope, out, &var.name);
}
}
BlockElem::Cmd { name, tail } => {
walk_any_vert_cmd_ref(name, scope, out);
walk_cmd_tail(tail, scope, out);
}
}
}
fn walk_cmd_tail(
t: &rustyfi_syntax::cst::ast::CmdTail,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::CmdTail;
match t {
CmdTail::Semi(_) => {}
CmdTail::Args { first, rest, .. } => {
walk_apparg(&first.0, scope, out);
for a in rest {
walk_apparg(&a.0, scope, out);
}
}
}
}
fn walk_math_elem(
m: &rustyfi_syntax::cst::ast::MathElemCst,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_math_bot(&m.base, scope, out);
for s in &m.scripts {
walk_math_script(s, scope, out);
}
}
fn walk_math_script(
s: &rustyfi_syntax::cst::ast::MathScript,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::MathScript;
match s {
MathScript::Super { group, .. } | MathScript::Sub { group, .. } => {
walk_math_group_arg(group, scope, out)
}
MathScript::Primes(_) => {}
}
}
fn walk_math_group_arg(
g: &rustyfi_syntax::cst::ast::MathGroupArg,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::MathGroupArg;
match g {
MathGroupArg::Group { elems, .. } => {
for m in elems {
walk_math_elem(&m.0, scope, out);
}
}
MathGroupArg::Bot(b) => walk_math_bot(b, scope, out),
}
}
fn walk_math_bot(
b: &rustyfi_syntax::cst::ast::MathBot,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::MathBot;
match b {
MathBot::Cmd { name, args } => {
walk_any_math_cmd_ref(name, scope, out);
for a in args {
walk_math_arg(a, scope, out);
}
}
MathBot::Chars(_) => {}
MathBot::Embed(v) => {
if v.mods.is_empty() {
emit_value(scope, out, &v.name);
}
}
MathBot::Sep(_) => {}
MathBot::Group { elems, .. } => {
for m in elems {
walk_math_elem(&m.0, scope, out);
}
}
}
}
fn walk_math_arg(
a: &rustyfi_syntax::cst::ast::MathArg,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::MathArg;
match a {
MathArg::Optional { body, .. } => walk_math_arg_body(body, scope, out),
MathArg::Omission(_) => {}
MathArg::Plain(body) => walk_math_arg_body(body, scope, out),
}
}
fn walk_math_arg_body(
b: &rustyfi_syntax::cst::ast::MathArgBody,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::MathArgBody;
match b {
MathArgBody::Math { elems, .. } => {
for m in elems {
walk_math_elem(&m.0, scope, out);
}
}
MathArgBody::Inline { elems, .. } => {
for el in elems {
walk_inline_elem(el, scope, out);
}
}
MathArgBody::Block { elems, .. } => {
for el in elems {
walk_block_elem(el, scope, out);
}
}
MathArgBody::ParenEscape { inner, .. } => walk_paren_body(inner, scope, out),
MathArgBody::ListEscape { items, .. } => {
for it in items {
walk_expr(&it.value.0, scope, out);
}
}
MathArgBody::RecordEscape { body, .. } => walk_record_body(body, scope, out),
}
}
fn walk_type_expr(
te: &rustyfi_syntax::cst::ast::TypeExpr,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::TypeExpr;
match te {
TypeExpr::Fun { opts, dom, cod, .. } => {
for o in opts {
walk_type_prod(&o.ty, scope, out);
}
walk_type_prod(dom, scope, out);
walk_type_expr(cod, scope, out);
}
TypeExpr::Atom(prod) => walk_type_prod(prod, scope, out),
TypeExpr::OptRowFun {
opt_dom, dom, cod, ..
} => {
for e in &opt_dom.entries {
walk_type_expr(&e.ty.0, scope, out);
}
walk_type_prod(dom, scope, out);
walk_type_expr(cod, scope, out);
}
}
}
fn walk_type_prod(
tp: &rustyfi_syntax::cst::ast::TypeProd,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_type_app(&tp.first, scope, out);
for st in &tp.rest {
walk_type_app(&st.ty, scope, out);
}
}
fn walk_type_app(
ta: &rustyfi_syntax::cst::ast::TypeApp,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
walk_type_atom(&ta.head, scope, out);
for a in &ta.rest {
walk_type_atom(a, scope, out);
}
}
fn walk_type_atom(
atom: &rustyfi_syntax::cst::ast::TypeAtom,
scope: &mut XverScope,
out: &mut FreeGlobals,
) {
use rustyfi_syntax::cst::ast::TypeAtom;
match atom {
TypeAtom::Cmd { args, .. } => {
for a in args {
for l in &a.opt_labels {
walk_type_expr(&l.ty.0, scope, out);
}
walk_type_expr(&a.ty.0, scope, out);
}
}
TypeAtom::Paren { inner, .. } => walk_type_expr(&inner.0, scope, out),
TypeAtom::Record { fields, .. } => {
for f in fields {
walk_type_expr(&f.ty.0, scope, out);
}
}
TypeAtom::Var(_) => {}
TypeAtom::Name(n) => emit_type(scope, out, &n.name),
TypeAtom::NameMod(_) => {}
TypeAtom::RecordOpen { inner, .. } => {
for f in &inner.fields {
walk_type_expr(&f.ty.0, scope, out);
}
}
}
}
struct OpenFrame {
id: DecoId,
x: Length,
marker_baseline: Length,
top: Option<Length>,
bottom: Option<Length>,
open_seq: usize,
carried: bool,
}
#[derive(Clone)]
struct OpenInlineFrame {
id: DecoId,
x: Length,
baseline_y: Length,
height: Length,
depth: Length,
carried: bool,
}
fn placed_line_right_edge(line: &rustyfi_backend::PlacedLine) -> Length {
let mut edge = Length::ZERO;
for (dx, bx) in &line.contents {
let right = *dx + bx.natural_width();
if right > edge {
edge = right;
}
}
line.x + edge
}
fn fire_inline_frame_fragment(
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
frame: &OpenInlineFrame,
right: Length,
deco_idx: usize,
) -> Result<(), eval::EvalError> {
if interp.fire_pass == eval::FirePass::HooksOnly {
return Ok(());
}
let (deco, deco_version) = match &interp.decos[frame.id.0] {
eval::DecoEntry::InlineBreakable {
decoset, version, ..
} => (decoset[deco_idx].clone(), *version),
_ => {
return eval::eval_error("BUG: non-breakable deco behind an inline frame marker");
}
};
let width = right - frame.x;
let pt = (frame.x, paper_height - frame.baseline_y);
interp.current_deco_id = Some(frame.id);
let gr = primitives::apply_deco(
interp,
deco_version,
deco,
pt,
width,
frame.height,
frame.depth,
)?;
interp.current_deco_id = None;
interp.page_graphics[page].extend(gr);
Ok(())
}
#[derive(Default)]
struct PlacedWalk {
next_open_seq: usize,
open: Vec<OpenFrame>,
open_inline: Vec<OpenInlineFrame>,
closings: Vec<(usize, Vec<GraphicsElem>)>,
}
impl PlacedWalk {
fn begin_page(&mut self) {
for f in &mut self.open {
f.top = None;
f.bottom = None;
}
self.closings.clear();
}
fn lines(
&mut self,
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
lines: &[rustyfi_backend::PlacedLine],
body: bool,
) -> Result<(), eval::EvalError> {
let page_number = (page + 1) as i64; for line in lines {
if body {
for f in &mut self.open_inline {
f.x = line.x;
f.baseline_y = line.baseline_y;
}
}
for (dx, bx) in &line.contents {
match bx {
PureHorzBox::HookPageBreak { id } => {
fire_page_break_hook(
interp,
paper_height,
page_number,
line.x + *dx,
line.baseline_y,
*id,
)?;
}
PureHorzBox::Frame { .. }
| PureHorzBox::Tabular(_)
| PureHorzBox::Graphics { .. } => {
fire_inline_frame(
interp,
paper_height,
page,
line.x + *dx,
line.baseline_y,
bx,
)?;
}
PureHorzBox::InlineFrameMarker {
id,
end: false,
height,
depth,
} => {
self.open_inline.push(OpenInlineFrame {
id: *id,
x: line.x + *dx,
baseline_y: line.baseline_y,
height: *height,
depth: *depth,
carried: false,
});
}
PureHorzBox::InlineFrameMarker { id, end: true, .. } => {
if let Some(pos) = self.open_inline.iter().rposition(|f| f.id == *id) {
let frame = self.open_inline.remove(pos);
let deco_idx = if frame.carried { 3 } else { 0 };
fire_inline_frame_fragment(
interp,
paper_height,
page,
&frame,
line.x + *dx,
deco_idx,
)?;
}
}
PureHorzBox::FrameMarker { id, end: false } => {
self.open.push(OpenFrame {
id: *id,
x: line.x + *dx,
marker_baseline: line.baseline_y,
top: None,
bottom: None,
open_seq: self.next_open_seq,
carried: false,
});
self.next_open_seq += 1;
}
PureHorzBox::FrameMarker { id, end: true } => {
if let Some(pos) = self.open.iter().rposition(|f| f.id == *id) {
let frame = self.open.remove(pos);
let deco_idx = if frame.carried { 3 } else { 0 };
let incl_top = true;
let gr = fire_block_frame_fragment(
interp,
paper_height,
&frame,
deco_idx,
incl_top,
true,
)?;
self.closings.push((frame.open_seq, gr));
}
}
PureHorzBox::EmbeddedBlock {
block, anchor_last, ..
} => {
fire_embedded_block_frames(
interp,
paper_height,
page,
line.x + *dx,
line.baseline_y,
block,
*anchor_last,
&mut self.next_open_seq,
&mut self.closings,
)?;
}
_ => {}
}
}
if body {
if let Some((height, depth)) = placed_line_extent(line) {
let top = line.baseline_y - height;
let bottom = line.baseline_y + depth;
for f in &mut self.open {
f.top = Some(f.top.map_or(top, |t| t.min(top)));
f.bottom = Some(f.bottom.map_or(bottom, |b| b.max(bottom)));
}
}
}
if body && !self.open_inline.is_empty() {
let right = placed_line_right_edge(line);
let pending: Vec<OpenInlineFrame> = self.open_inline.clone();
for frame in &pending {
let deco_idx = if frame.carried { 2 } else { 1 };
fire_inline_frame_fragment(
interp,
paper_height,
page,
frame,
right,
deco_idx,
)?;
}
for f in &mut self.open_inline {
f.carried = true;
}
}
}
Ok(())
}
fn end_page(
&mut self,
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
) -> Result<(), eval::EvalError> {
let mut page_end_fires: Vec<(usize, Vec<GraphicsElem>)> = Vec::new();
let mut fired_seqs: Vec<usize> = Vec::new();
for frame in &self.open {
if frame.top.is_none() && frame.bottom.is_none() {
continue;
}
let deco_idx = if frame.carried { 2 } else { 1 };
let gr = fire_block_frame_fragment(interp, paper_height, frame, deco_idx, true, false)?;
page_end_fires.push((frame.open_seq, gr));
fired_seqs.push(frame.open_seq);
}
for f in &mut self.open {
if fired_seqs.contains(&f.open_seq) {
f.carried = true;
}
}
self.closings.extend(page_end_fires);
self.closings.sort_by_key(|(seq, _)| *seq);
for (_, gr) in std::mem::take(&mut self.closings) {
if let Some(slot) = interp.page_graphics.get_mut(page) {
slot.extend(gr);
}
}
Ok(())
}
}
pub fn fire_hooks(interp: &mut eval::Interp, doc: &DocumentValue) -> Result<(), eval::EvalError> {
interp.page_graphics = doc.pages.iter().map(|_| Vec::new()).collect();
let paper_height = doc.geometry.paper_height;
interp.fire_pass = if interp.page_break_hooks_fired {
eval::FirePass::DecosOnly
} else {
eval::FirePass::All
};
let mut walk = PlacedWalk::default();
let result = (|| {
for (i, page) in doc.pages.iter().enumerate() {
interp.current_page = Some(i);
walk.begin_page();
let split = page.body_lines.min(page.lines.len());
walk.lines(interp, paper_height, i, &page.lines[..split], true)?;
walk.lines(interp, paper_height, i, &page.lines[split..], false)?;
walk.end_page(interp, paper_height, i)?;
}
Ok(())
})();
interp.fire_pass = eval::FirePass::All;
interp.current_page = None;
result
}
fn fire_block_frame_fragment(
interp: &mut eval::Interp,
paper_height: Length,
frame: &OpenFrame,
deco_idx: usize,
incl_top_pad: bool,
incl_bot_pad: bool,
) -> Result<Vec<GraphicsElem>, eval::EvalError> {
if interp.fire_pass == eval::FirePass::HooksOnly {
return Ok(Vec::new());
}
let (pads, width, deco, deco_version) = match &interp.decos[frame.id.0] {
eval::DecoEntry::Block {
pads,
width,
decoset,
version,
} => (*pads, *width, decoset[deco_idx].clone(), *version),
eval::DecoEntry::Inline { .. } | eval::DecoEntry::InlineBreakable { .. } => {
return eval::eval_error("BUG: inline deco behind a block-frame marker")
}
};
let top = frame.top.unwrap_or(frame.marker_baseline);
let bottom = frame.bottom.unwrap_or(frame.marker_baseline);
let frame_top = if incl_top_pad { top - pads.t } else { top };
let frame_bottom = if incl_bot_pad {
bottom + pads.b
} else {
bottom
};
let pt = (frame.x, paper_height - frame_bottom);
interp.current_deco_id = Some(frame.id);
let height = frame_bottom - frame_top;
let gr = primitives::apply_deco(interp, deco_version, deco, pt, width, height, Length::ZERO)?;
interp.current_deco_id = None;
if deco_idx == 0 && !gr.is_empty() {
let back = (Length::ZERO - pt.0, Length::ZERO - pt.1);
interp.frame_decos.push((
frame.id,
rustyfi_backend::FrameDecoration {
width,
height,
pads: (pads.l, pads.r, pads.t, pads.b),
elems: gr.iter().map(|e| shift_graphics(back, e)).collect(),
},
));
}
Ok(gr)
}
#[allow(clippy::too_many_arguments)]
fn fire_embedded_block_frames(
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
tx: Length,
baseline_ydown: Length,
block: &[VertBox],
anchor_last: bool,
next_open_seq: &mut usize,
out: &mut Vec<(usize, Vec<GraphicsElem>)>,
) -> Result<(), eval::EvalError> {
let placed = place_block_at((Length::ZERO, Length::ZERO), block.to_vec());
let anchor = if anchor_last {
placed.last()
} else {
placed.first()
};
let Some(anchor) = anchor else {
return Ok(());
};
let anchor_offset = anchor.baseline_y;
let mut open: Vec<OpenFrame> = Vec::new();
let mut open_inline: Vec<OpenInlineFrame> = Vec::new();
for pl in &placed {
let abs_baseline = baseline_ydown + (pl.baseline_y - anchor_offset);
for f in &mut open_inline {
f.x = tx + pl.x;
f.baseline_y = abs_baseline;
}
for (dx, bx) in &pl.contents {
match bx {
PureHorzBox::InlineFrameMarker {
id,
end: false,
height,
depth,
} => {
open_inline.push(OpenInlineFrame {
id: *id,
x: tx + pl.x + *dx,
baseline_y: abs_baseline,
height: *height,
depth: *depth,
carried: false,
});
}
PureHorzBox::InlineFrameMarker { id, end: true, .. } => {
if let Some(pos) = open_inline.iter().rposition(|f| f.id == *id) {
let frame = open_inline.remove(pos);
let deco_idx = if frame.carried { 3 } else { 0 };
fire_inline_frame_fragment(
interp,
paper_height,
page,
&frame,
tx + pl.x + *dx,
deco_idx,
)?;
}
}
PureHorzBox::FrameMarker { id, end: false } => {
open.push(OpenFrame {
id: *id,
x: tx + pl.x + *dx,
marker_baseline: abs_baseline,
top: None,
bottom: None,
open_seq: *next_open_seq,
carried: false,
});
*next_open_seq += 1;
}
PureHorzBox::FrameMarker { id, end: true } => {
if let Some(pos) = open.iter().rposition(|f| f.id == *id) {
let frame = open.remove(pos);
let gr = fire_block_frame_fragment(interp, paper_height, &frame, 0, true, true)?;
out.push((frame.open_seq, gr));
}
}
PureHorzBox::Frame { .. }
| PureHorzBox::Tabular(_)
| PureHorzBox::Graphics { .. } => {
fire_inline_frame(interp, paper_height, page, tx + pl.x + *dx, abs_baseline, bx)?;
}
PureHorzBox::EmbeddedBlock {
block: inner,
anchor_last: al,
..
} => {
fire_embedded_block_frames(
interp,
paper_height,
page,
tx + pl.x + *dx,
abs_baseline,
inner,
*al,
next_open_seq,
out,
)?;
}
_ => {}
}
}
if let Some((height, depth)) = placed_line_extent(pl) {
let top = abs_baseline - height;
let bottom = abs_baseline + depth;
for f in &mut open {
f.top = Some(f.top.map_or(top, |t| t.min(top)));
f.bottom = Some(f.bottom.map_or(bottom, |b| b.max(bottom)));
}
}
if !open_inline.is_empty() {
let right = tx + placed_line_right_edge(pl);
let pending: Vec<OpenInlineFrame> = open_inline.clone();
for frame in &pending {
let deco_idx = if frame.carried { 2 } else { 1 };
fire_inline_frame_fragment(interp, paper_height, page, frame, right, deco_idx)?;
}
for f in &mut open_inline {
f.carried = true;
}
}
}
Ok(())
}
fn fire_page_break_hook(
interp: &mut eval::Interp,
paper_height: Length,
page_number: i64,
x: Length,
baseline_y: Length,
id: rustyfi_backend::HookId,
) -> Result<(), eval::EvalError> {
if interp.fire_pass == eval::FirePass::DecosOnly {
return Ok(());
}
let closure = interp.hooks[id.0].clone();
let mut fields = BTreeMap::new();
fields.insert("page-number".to_string(), Value::Int(page_number));
let pbinfo = Value::Record(fields);
let point = Value::Tuple(vec![
Value::Length(x),
Value::Length(paper_height - baseline_y),
]);
let applied = interp.apply(closure, pbinfo)?;
match interp.apply(applied, point)? {
Value::Unit => Ok(()),
other => eval::eval_error(format!(
"hook-page-break closure returned {}, expected unit",
other.type_name()
)),
}
}
fn fire_inline_frame(
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
x: Length,
baseline_y: Length,
bx: &PureHorzBox,
) -> Result<(), eval::EvalError> {
let contents = match bx {
PureHorzBox::Frame {
width,
height,
depth,
deco,
contents,
} => {
if interp.fire_pass != eval::FirePass::HooksOnly {
let (deco_v, deco_version) = match &interp.decos[deco.0] {
eval::DecoEntry::Inline { deco, version } => (deco.clone(), *version),
eval::DecoEntry::Block { .. } | eval::DecoEntry::InlineBreakable { .. } => {
return eval::eval_error("BUG: block deco behind an inline frame")
}
};
let pt = (x, paper_height - baseline_y);
interp.current_deco_id = Some(*deco);
let gr = primitives::apply_deco(
interp,
deco_version,
deco_v,
pt,
*width,
*height,
*depth,
)?;
interp.current_deco_id = None;
interp.page_graphics[page].extend(gr);
}
contents
}
PureHorzBox::Tabular(tab) => {
for cell in &tab.cells {
fire_nested_in_contents(
interp,
paper_height,
page,
x + cell.x,
baseline_y - cell.baseline_y,
&cell.contents,
)?;
}
return Ok(());
}
PureHorzBox::Graphics {
elems,
origin_independent,
..
} => {
let anchor_y = if *origin_independent {
paper_height
} else {
baseline_y
};
let anchor_x = if *origin_independent { Length::ZERO } else { x };
fire_nested_in_graphics(interp, paper_height, page, anchor_x, anchor_y, elems)?;
return Ok(());
}
_ => return Ok(()),
};
fire_nested_in_contents(interp, paper_height, page, x, baseline_y, contents)
}
fn fire_nested_in_graphics(
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
anchor_x: Length,
anchor_y: Length,
elems: &[GraphicsElem],
) -> Result<(), eval::EvalError> {
for elem in elems {
match elem {
GraphicsElem::Text { pt, contents, .. } => {
fire_nested_in_contents(
interp,
paper_height,
page,
anchor_x + pt.0,
anchor_y - pt.1,
contents,
)?;
}
GraphicsElem::Group(inner) | GraphicsElem::Clip(_, inner) => {
fire_nested_in_graphics(interp, paper_height, page, anchor_x, anchor_y, inner)?;
}
GraphicsElem::Destination { key, pt } => {
if interp.fire_pass == eval::FirePass::HooksOnly {
continue;
}
let name = interp.dest_name(key);
let x = anchor_x + pt.0;
let y = paper_height - anchor_y + pt.1;
if let Some(deco_id) = interp.current_deco_id {
interp.dest_decos.push((deco_id, name.clone()));
}
interp.destinations.push(rustyfi_backend::NamedDest {
page,
name,
x,
y,
});
}
GraphicsElem::Fill(..) | GraphicsElem::Stroke(..) | GraphicsElem::DashedStroke(..) => {}
}
}
Ok(())
}
fn fire_nested_in_contents(
interp: &mut eval::Interp,
paper_height: Length,
page: usize,
x0: Length,
baseline_y: Length,
contents: &[(Length, PureHorzBox)],
) -> Result<(), eval::EvalError> {
let mut open_inline: Vec<OpenInlineFrame> = Vec::new();
for (dx, child) in contents {
if let PureHorzBox::HookPageBreak { id } = child {
fire_page_break_hook(interp, paper_height, (page + 1) as i64, x0 + *dx, baseline_y, *id)?;
}
match child {
PureHorzBox::InlineFrameMarker {
id,
end: false,
height,
depth,
} => open_inline.push(OpenInlineFrame {
id: *id,
x: x0 + *dx,
baseline_y,
height: *height,
depth: *depth,
carried: false,
}),
PureHorzBox::InlineFrameMarker { id, end: true, .. } => {
if let Some(pos) = open_inline.iter().rposition(|f| f.id == *id) {
let frame = open_inline.remove(pos);
fire_inline_frame_fragment(interp, paper_height, page, &frame, x0 + *dx, 0)?;
}
}
_ => {}
}
fire_inline_frame(interp, paper_height, page, x0 + *dx, baseline_y, child)?;
}
Ok(())
}