Skip to main content

yog_script_analyze/
lib.rs

1//! Yog Script (`.ys`) and Yog Nanite Script (`.yns`): the language a Yog
2//! mod is written in (`docs/design/ys.md`, `docs/design/ys-grammar.md`).
3//! This crate reads a source into its syntax tree, resolves its names and
4//! says what's wrong with it, as Diagnostics placed in the source.
5
6mod source;
7
8pub use source::token;
9
10pub mod ast;
11mod borrowck;
12pub mod index;
13mod lexer;
14pub mod macros;
15pub mod parser;
16pub mod resolve;
17pub mod system;
18mod typeck;
19
20pub use typeck::{Type, Types};
21pub mod versions;
22pub mod words;
23pub mod yog;
24
25use std::path::Path;
26
27use yog_data::Diagnostic;
28use yog_ids::Format;
29
30/// A source read: its tree (what could be read of it) and its problems.
31#[derive(Debug)]
32pub struct Parsed {
33    pub file: ast::File,
34    pub problems: Vec<Diagnostic>,
35}
36
37/// A source checked: its tree, what its names stand for, its problems.
38#[derive(Debug)]
39pub struct Checked {
40    pub file: ast::File,
41    pub symbols: resolve::Symbols,
42    pub problems: Vec<Diagnostic>,
43    /// Each expression's type (none when the source had errors before types).
44    pub types: Types,
45}
46
47/// The source `src` of the file `name` read; patches are allowed when it's
48/// a `.yns` file (read as the newest game version Yog runs on).
49#[must_use]
50pub fn parse(name: &str, src: &str) -> Parsed {
51    let mut report = yog_analysis::Report::new(name, src);
52    let file = read(name, src, (versions::newest(), None), &mut report);
53    Parsed {
54        file,
55        problems: report.out,
56    }
57}
58
59/// The source `src` of the file `name` read, its names resolved and its
60/// types checked (the game's unknown): what the editors report.
61#[must_use]
62pub fn check(name: &str, src: &str) -> Checked {
63    check_with(name, src, None)
64}
65
66/// [`check`] of a `.yns` with the game's classes of one of the versions
67/// it targets: what `yog build` reports, once a version. A `.ys` reaches
68/// Yog, not the game: `game` is ignored for it.
69#[must_use]
70pub fn check_with(name: &str, src: &str, game: Option<&index::ClassIndex>) -> Checked {
71    check_in(
72        name,
73        src,
74        Env {
75            game,
76            ..Env::default()
77        },
78    )
79}
80
81/// What a source is checked in: the game's classes (a `.yns`'s, of one
82/// version), the build's procedural macros.
83#[derive(Clone, Copy, Default)]
84pub struct Env<'a> {
85    pub game: Option<&'a index::ClassIndex>,
86    pub macros: Option<&'a dyn macros::ProcMacros>,
87    /// The mod says it sends to the server (`yog.toml`: `[network] to_server`).
88    pub to_server: bool,
89}
90
91/// [`check_with`] in `env`.
92#[must_use]
93pub fn check_in(name: &str, src: &str, env: Env<'_>) -> Checked {
94    let game = env.game;
95    let mut report = yog_analysis::Report::new(name, src);
96    // A `.yns` is read as the version whose classes it's checked with.
97    let mc = game.map_or(versions::newest(), |g| g.mc.as_str());
98    let file = read(name, src, (mc, env.macros), &mut report);
99    let symbols = resolve::resolve(&file, &mut report);
100    let game = game.filter(|_| is_nanites(name));
101    // Types are checked in what reads and resolves cleanly: a broken tree
102    // would only report the same problem again.
103    let mut types = Types::default();
104    if !report.out.iter().any(yog_data::Diagnostic::is_error) {
105        let side = typeck::Allowed {
106            nanites: is_nanites(name),
107            to_server: env.to_server,
108        };
109        types = typeck::check(&file, side, game, &mut report);
110        borrowck::check(&file, &mut report);
111    }
112    Checked {
113        file,
114        symbols,
115        problems: report.out,
116        types,
117    }
118}
119
120/// The source read as game version `mc`, with the build's macros `procs`.
121fn read(
122    name: &str,
123    src: &str,
124    (mc, procs): (&str, Option<&dyn macros::ProcMacros>),
125    report: &mut yog_analysis::Report<'_>,
126) -> ast::File {
127    let nanites = is_nanites(name);
128    let text = preprocessed(src, mc, nanites, report);
129    let tokens = macros::expand(lexer::lex(&text, report), &text, procs, report);
130    let file = parser::Parser::new(&tokens, report, nanites).file();
131    check_targets(&file, nanites, report);
132    file
133}
134
135/// `src` as game version `mc` reads it: a `.yns`'s `#if MC >= 1.21`
136/// directives (C#'s, by the toolkit's preprocessor, which knows nothing of
137/// Yog: it's handed the variable) applied; a `.ys` has none, as it runs on
138/// every version unchanged.
139fn preprocessed(
140    src: &str,
141    mc: &str,
142    nanites: bool,
143    report: &mut yog_analysis::Report<'_>,
144) -> String {
145    use yog_analysis::preprocess::{Defines, Problem};
146    use yog_ids::script_problem::DIRECTIVE;
147    let defines: Defines = if nanites {
148        [(yog_ids::script_define::GAME.to_string(), mc.to_string())].into()
149    } else {
150        Defines::new()
151    };
152    let out = yog_analysis::preprocess::preprocess(src, &defines);
153    for (at, problem) in out.problems {
154        let span = source::token::Span::new(at.start, at.end);
155        let message = match problem {
156            Problem::Unmatched => "no `#if` before it, or after `#else`".to_string(),
157            Problem::Unterminated => "this `#if` never ends: `#endif`".to_string(),
158            Problem::BadCondition(why) => format!("the condition doesn't read: {why}"),
159        };
160        report.error(DIRECTIVE, span, message);
161    }
162    if out.directives && !nanites {
163        report.error(
164            DIRECTIVE,
165            source::token::Span::default(),
166            "a `.ys` runs on every game version unchanged: directives (`#if`) are for a `.yns`",
167        );
168    }
169    out.text
170}
171
172/// Whether the file `name` is Yog Nanite Script (`.yns`): patches allowed,
173/// the game's classes reachable.
174#[must_use]
175pub fn is_nanites(name: &str) -> bool {
176    Format::Nanites.is(Path::new(name))
177}
178
179/// The game versions a `.yns` says it's for (`targets mc …;`).
180#[must_use]
181pub fn targets(file: &ast::File) -> Option<&ast::VersionCond> {
182    file.decls.iter().find_map(|d| match &d.kind {
183        ast::DeclKind::Targets(cond) => Some(cond),
184        _ => None,
185    })
186}
187
188/// A `.yns` names its versions once; a `.ys` doesn't (the mod does).
189fn check_targets(file: &ast::File, nanites: bool, report: &mut yog_analysis::Report<'_>) {
190    use yog_ids::script_problem::TARGETS;
191    let all: Vec<_> = file
192        .decls
193        .iter()
194        .filter(|d| matches!(d.kind, ast::DeclKind::Targets(_)))
195        .collect();
196    match (nanites, all.as_slice()) {
197        (true, []) => {
198            let all = yog_ids::game_version::ALL_VALUES;
199            let (oldest, newest) = (all.first().unwrap_or(&""), all.last().unwrap_or(&""));
200            report.error(
201                TARGETS,
202                source::token::Span::default(),
203                format!("a `.yns` says which game versions its patches are for: `targets mc {oldest}..{newest};`"),
204            );
205        }
206        (true, [_, again, ..]) => report.error(TARGETS, again.span, "`targets` is said once a file"),
207        (false, [first, ..]) => report.error(
208            TARGETS,
209            first.span,
210            "`targets` is for `.yns` files: a `.ys` reaches Yog, not the game, so it runs on the mod's every version",
211        ),
212        _ => {}
213    }
214}