mod source;
pub use source::token;
pub mod ast;
mod borrowck;
pub mod index;
mod lexer;
pub mod macros;
pub mod parser;
pub mod resolve;
pub mod system;
mod typeck;
pub use typeck::{Type, Types};
pub mod versions;
pub mod words;
pub mod yog;
use std::path::Path;
use yog_data::Diagnostic;
use yog_ids::Format;
#[derive(Debug)]
pub struct Parsed {
pub file: ast::File,
pub problems: Vec<Diagnostic>,
}
#[derive(Debug)]
pub struct Checked {
pub file: ast::File,
pub symbols: resolve::Symbols,
pub problems: Vec<Diagnostic>,
pub types: Types,
}
#[must_use]
pub fn parse(name: &str, src: &str) -> Parsed {
let mut report = yog_analysis::Report::new(name, src);
let file = read(name, src, (versions::newest(), None), &mut report);
Parsed {
file,
problems: report.out,
}
}
#[must_use]
pub fn check(name: &str, src: &str) -> Checked {
check_with(name, src, None)
}
#[must_use]
pub fn check_with(name: &str, src: &str, game: Option<&index::ClassIndex>) -> Checked {
check_in(
name,
src,
Env {
game,
..Env::default()
},
)
}
#[derive(Clone, Copy, Default)]
pub struct Env<'a> {
pub game: Option<&'a index::ClassIndex>,
pub macros: Option<&'a dyn macros::ProcMacros>,
pub to_server: bool,
}
#[must_use]
pub fn check_in(name: &str, src: &str, env: Env<'_>) -> Checked {
let game = env.game;
let mut report = yog_analysis::Report::new(name, src);
let mc = game.map_or(versions::newest(), |g| g.mc.as_str());
let file = read(name, src, (mc, env.macros), &mut report);
let symbols = resolve::resolve(&file, &mut report);
let game = game.filter(|_| is_nanites(name));
let mut types = Types::default();
if !report.out.iter().any(yog_data::Diagnostic::is_error) {
let side = typeck::Allowed {
nanites: is_nanites(name),
to_server: env.to_server,
};
types = typeck::check(&file, side, game, &mut report);
borrowck::check(&file, &mut report);
}
Checked {
file,
symbols,
problems: report.out,
types,
}
}
fn read(
name: &str,
src: &str,
(mc, procs): (&str, Option<&dyn macros::ProcMacros>),
report: &mut yog_analysis::Report<'_>,
) -> ast::File {
let nanites = is_nanites(name);
let text = preprocessed(src, mc, nanites, report);
let tokens = macros::expand(lexer::lex(&text, report), &text, procs, report);
let file = parser::Parser::new(&tokens, report, nanites).file();
check_targets(&file, nanites, report);
file
}
fn preprocessed(
src: &str,
mc: &str,
nanites: bool,
report: &mut yog_analysis::Report<'_>,
) -> String {
use yog_analysis::preprocess::{Defines, Problem};
use yog_ids::script_problem::DIRECTIVE;
let defines: Defines = if nanites {
[(yog_ids::script_define::GAME.to_string(), mc.to_string())].into()
} else {
Defines::new()
};
let out = yog_analysis::preprocess::preprocess(src, &defines);
for (at, problem) in out.problems {
let span = source::token::Span::new(at.start, at.end);
let message = match problem {
Problem::Unmatched => "no `#if` before it, or after `#else`".to_string(),
Problem::Unterminated => "this `#if` never ends: `#endif`".to_string(),
Problem::BadCondition(why) => format!("the condition doesn't read: {why}"),
};
report.error(DIRECTIVE, span, message);
}
if out.directives && !nanites {
report.error(
DIRECTIVE,
source::token::Span::default(),
"a `.ys` runs on every game version unchanged: directives (`#if`) are for a `.yns`",
);
}
out.text
}
#[must_use]
pub fn is_nanites(name: &str) -> bool {
Format::Nanites.is(Path::new(name))
}
#[must_use]
pub fn targets(file: &ast::File) -> Option<&ast::VersionCond> {
file.decls.iter().find_map(|d| match &d.kind {
ast::DeclKind::Targets(cond) => Some(cond),
_ => None,
})
}
fn check_targets(file: &ast::File, nanites: bool, report: &mut yog_analysis::Report<'_>) {
use yog_ids::script_problem::TARGETS;
let all: Vec<_> = file
.decls
.iter()
.filter(|d| matches!(d.kind, ast::DeclKind::Targets(_)))
.collect();
match (nanites, all.as_slice()) {
(true, []) => {
let all = yog_ids::game_version::ALL_VALUES;
let (oldest, newest) = (all.first().unwrap_or(&""), all.last().unwrap_or(&""));
report.error(
TARGETS,
source::token::Span::default(),
format!("a `.yns` says which game versions its patches are for: `targets mc {oldest}..{newest};`"),
);
}
(true, [_, again, ..]) => report.error(TARGETS, again.span, "`targets` is said once a file"),
(false, [first, ..]) => report.error(
TARGETS,
first.span,
"`targets` is for `.yns` files: a `.ys` reaches Yog, not the game, so it runs on the mod's every version",
),
_ => {}
}
}