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::commands::command_arg;
21pub use typeck::{
22    Message, MethodUse, OperatorUse, Quoted, Type, Types, body_yields, builtin_type, content_path,
23    MessageTemplate, holes, item_of, list_item, leaves, system_name, system_type,
24};
25pub mod versions;
26pub mod words;
27
28/// How Yog Script spells what its reader reads: what a writer of it
29/// (`yog-codegen-ys`) writes the same way.
30pub mod spelling {
31    pub use crate::lexer::string::{CLOSE, ESCAPE, ESCAPES, OPEN, QUOTE, UNICODE_OPEN};
32    pub use crate::lexer::VERBATIM;
33
34    /// Whether `name` is reserved: a keyword, written `@name` as a name.
35    #[must_use]
36    pub fn is_keyword(name: &str) -> bool {
37        crate::words::reserved(name).is_some()
38    }
39}
40pub mod yog;
41
42use std::path::Path;
43
44use yog_data::Diagnostic;
45use yog_ids::Format;
46
47/// A source read: its tree (what could be read of it) and its problems.
48#[derive(Debug)]
49pub struct Parsed {
50    pub file: ast::File,
51    pub problems: Vec<Diagnostic>,
52}
53
54/// A source checked: its tree, what its names stand for, its problems.
55#[derive(Debug)]
56pub struct Checked {
57    pub file: ast::File,
58    pub symbols: resolve::Symbols,
59    pub problems: Vec<Diagnostic>,
60    /// Each expression's type (none when the source had errors before types).
61    pub types: Types,
62}
63
64/// The source `src` of the file `name` read; patches are allowed when it's
65/// a `.yns` file (read as the newest game version Yog runs on).
66#[must_use]
67pub fn parse(name: &str, src: &str) -> Parsed {
68    let mut report = yog_analysis::Report::new(name, src);
69    let file = read(name, src, (versions::newest(), Env::default()), &mut report);
70    Parsed {
71        file,
72        problems: report.out,
73    }
74}
75
76/// The source `src` of the file `name` read, its names resolved and its
77/// types checked (the game's unknown): what the editors report.
78#[must_use]
79pub fn check(name: &str, src: &str) -> Checked {
80    check_with(name, src, None)
81}
82
83/// [`check`] of a `.yns` with the game's classes of one of the versions
84/// it targets: what `yog build` reports, once a version. A `.ys` reaches
85/// Yog, not the game: `game` is ignored for it.
86#[must_use]
87pub fn check_with(name: &str, src: &str, game: Option<&index::ClassIndex>) -> Checked {
88    check_in(
89        name,
90        src,
91        Env {
92            game,
93            ..Env::default()
94        },
95    )
96}
97
98/// What a source is checked in: the game's classes (a `.yns`'s, of one
99/// version), the build's procedural macros.
100#[derive(Clone, Copy, Default)]
101pub struct Env<'a> {
102    pub game: Option<&'a index::ClassIndex>,
103    pub macros: Option<&'a dyn macros::ProcMacros>,
104    /// The mod's files, for `include_str!`.
105    pub files: Option<&'a dyn macros::ModFiles>,
106    /// The mod says it sends to the server (`yog.toml`: `[network] to_server`).
107    pub to_server: bool,
108    /// The mod's content files, each (kind, id) — what its content
109    /// declarations bind to; `None`: not known (not checked).
110    pub content: Option<&'a [(String, String)]>,
111    /// The mod's other sources' declarations ([`declarations`]): its
112    /// project, whose types, extensions and events this source reaches as
113    /// a C# project's files reach each other's.
114    pub others: &'a [&'a [ast::Decl]],
115}
116
117/// Source `src`'s declarations, read (macros expanded) as its check reads
118/// them: what the other sources of its mod reach ([`Env::others`]). What's
119/// wrong with it is its own check's to report.
120#[must_use]
121pub fn declarations(name: &str, src: &str, env: Env<'_>) -> Vec<ast::Decl> {
122    let mut report = yog_analysis::Report::new(name, src);
123    let mc = env.game.map_or(versions::newest(), |g| g.mc.as_str());
124    read(name, src, (mc, env), &mut report).decls
125}
126
127/// [`check_with`] in `env`.
128#[must_use]
129pub fn check_in(name: &str, src: &str, env: Env<'_>) -> Checked {
130    let game = env.game;
131    let mut report = yog_analysis::Report::new(name, src);
132    // A `.yns` is read as the version whose classes it's checked with.
133    let mc = game.map_or(versions::newest(), |g| g.mc.as_str());
134    let file = read(name, src, (mc, env), &mut report);
135    let symbols = resolve::resolve(&file, &mut report);
136    let game = game.filter(|_| is_nanites(name));
137    // Types are checked in what reads and resolves cleanly: a broken tree
138    // would only report the same problem again.
139    let mut types = Types::default();
140    if !report.out.iter().any(yog_data::Diagnostic::is_error) {
141        let side = typeck::Allowed {
142            nanites: is_nanites(name),
143            to_server: env.to_server,
144            content: env.content.map(<[_]>::to_vec),
145        };
146        types = typeck::check(&file, side, (game, env.others), &mut report);
147        borrowck::check(&file, &types, &mut report);
148    }
149    Checked {
150        file,
151        symbols,
152        problems: report.out,
153        types,
154    }
155}
156
157/// The source read as game version `mc`, with the build's macros and files (`env`).
158fn read(
159    name: &str,
160    src: &str,
161    (mc, env): (&str, Env<'_>),
162    report: &mut yog_analysis::Report<'_>,
163) -> ast::File {
164    let nanites = is_nanites(name);
165    let text = preprocessed(src, mc, nanites, report);
166    let tokens = macros::expand(
167        lexer::lex(&text, report),
168        (&text, name),
169        (env.macros, env.files),
170        report,
171    );
172    let file = parser::Parser::new(&tokens, report, nanites).file();
173    check_targets(&file, nanites, report);
174    file
175}
176
177/// `src` as game version `mc` reads it: a `.yns`'s `#if MC >= 1.21`
178/// directives (C#'s, by the toolkit's preprocessor, which knows nothing of
179/// Yog: it's handed the variable) applied; a `.ys` has none, as it runs on
180/// every version unchanged.
181fn preprocessed(
182    src: &str,
183    mc: &str,
184    nanites: bool,
185    report: &mut yog_analysis::Report<'_>,
186) -> String {
187    use yog_analysis::preprocess::{Defines, Problem};
188    use yog_ids::script_problem::DIRECTIVE;
189    let defines: Defines = if nanites {
190        [(yog_ids::script_define::GAME.to_string(), mc.to_string())].into()
191    } else {
192        Defines::new()
193    };
194    let out = yog_analysis::preprocess::preprocess(src, &defines);
195    for (at, problem) in out.problems {
196        let span = source::token::Span::new(at.start, at.end);
197        let message = match problem {
198            Problem::Unmatched => "no `#if` before it, or after `#else`".to_string(),
199            Problem::Unterminated => "this `#if` never ends: `#endif`".to_string(),
200            Problem::BadCondition(why) => format!("the condition doesn't read: {why}"),
201        };
202        report.error(DIRECTIVE, span, message);
203    }
204    if out.directives && !nanites {
205        report.error(
206            DIRECTIVE,
207            source::token::Span::default(),
208            "a `.ys` runs on every game version unchanged: directives (`#if`) are for a `.yns`",
209        );
210    }
211    out.text
212}
213
214/// Whether the file `name` is Yog Nanite Script (`.yns`): patches allowed,
215/// the game's classes reachable.
216#[must_use]
217pub fn is_nanites(name: &str) -> bool {
218    Format::Nanites.is(Path::new(name))
219}
220
221/// The game versions a `.yns` says it's for (`targets mc …;`).
222#[must_use]
223pub fn targets(file: &ast::File) -> Option<&ast::VersionCond> {
224    file.decls.iter().find_map(|d| match &d.kind {
225        ast::DeclKind::Targets(cond) => Some(cond),
226        _ => None,
227    })
228}
229
230/// A `.yns` names its versions once; a `.ys` doesn't (the mod does).
231fn check_targets(file: &ast::File, nanites: bool, report: &mut yog_analysis::Report<'_>) {
232    use yog_ids::script_problem::TARGETS;
233    let all: Vec<_> = file
234        .decls
235        .iter()
236        .filter(|d| matches!(d.kind, ast::DeclKind::Targets(_)))
237        .collect();
238    match (nanites, all.as_slice()) {
239        (true, []) => {
240            let all = yog_ids::game_version::ALL_VALUES;
241            let (oldest, newest) = (all.first().unwrap_or(&""), all.last().unwrap_or(&""));
242            report.error(
243                TARGETS,
244                source::token::Span::default(),
245                format!("a `.yns` says which game versions its patches are for: `targets mc {oldest}..{newest};`"),
246            );
247        }
248        (true, [_, again, ..]) => report.error(TARGETS, again.span, "`targets` is said once a file"),
249        (false, [first, ..]) => report.error(
250            TARGETS,
251            first.span,
252            "`targets` is for `.yns` files: a `.ys` reaches Yog, not the game, so it runs on the mod's every version",
253        ),
254        _ => {}
255    }
256}
257
258/// The library class content of kind `kind` inherits (`block`'s
259/// `BlockObject`), when its kind has one.
260#[must_use]
261pub fn content_base(kind: &str) -> Option<&'static str> {
262    match kind {
263        k if k == yog_ids::kind::BLOCK => Some(yog_script_ids::system::BLOCK_OBJECT),
264        k if k == yog_ids::kind::ITEM => Some(yog_script_ids::system::ITEM_OBJECT),
265        _ => None,
266    }
267}