1mod 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#[derive(Debug)]
32pub struct Parsed {
33 pub file: ast::File,
34 pub problems: Vec<Diagnostic>,
35}
36
37#[derive(Debug)]
39pub struct Checked {
40 pub file: ast::File,
41 pub symbols: resolve::Symbols,
42 pub problems: Vec<Diagnostic>,
43 pub types: Types,
45}
46
47#[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#[must_use]
62pub fn check(name: &str, src: &str) -> Checked {
63 check_with(name, src, None)
64}
65
66#[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#[derive(Clone, Copy, Default)]
84pub struct Env<'a> {
85 pub game: Option<&'a index::ClassIndex>,
86 pub macros: Option<&'a dyn macros::ProcMacros>,
87 pub to_server: bool,
89}
90
91#[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 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 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
120fn 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
135fn 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#[must_use]
175pub fn is_nanites(name: &str) -> bool {
176 Format::Nanites.is(Path::new(name))
177}
178
179#[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
188fn 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}