mcvm_parse/
instruction.rs

1use std::fmt::Display;
2
3use anyhow::bail;
4use mcvm_shared::later::Later;
5use mcvm_shared::modifications::{ModloaderMatch, PluginLoaderMatch};
6use mcvm_shared::pkg::PackageAddonHashes;
7use mcvm_shared::util::yes_no;
8use mcvm_shared::versions::VersionPattern;
9use mcvm_shared::Side;
10
11use super::conditions::Condition;
12use super::lex::{TextPos, Token};
13use super::parse::BlockId;
14use super::vars::Value;
15use super::FailReason;
16use crate::conditions::{ArchCondition, OSCondition};
17use crate::unexpected_token;
18use mcvm_shared::addon::AddonKind;
19
20/// A command / statement run in a package script
21#[derive(Debug, Clone)]
22pub struct Instruction {
23	/// What type of instruction this is
24	pub kind: InstrKind,
25	/// The textual position of the instruction
26	pub pos: TextPos,
27}
28
29/// Type of an instruction
30#[derive(Debug, Clone)]
31pub enum InstrKind {
32	/// Check conditions
33	If {
34		/// The condition to check
35		condition: Condition,
36		/// The block to run if the condition succeeds
37		if_block: BlockId,
38		/// The chain of else blocks to run if the initial condition fails
39		else_blocks: Vec<ElseBlock>,
40	},
41	/// Set the package name metadata
42	Name(Later<String>),
43	/// Set the package description metadata
44	Description(Later<String>),
45	/// Set the package long description metadata
46	LongDescription(Later<String>),
47	/// Set the package authors metadata
48	Authors(Vec<String>),
49	/// Set the package maintainers metadata
50	PackageMaintainers(Vec<String>),
51	/// Set the package website metadata
52	Website(Later<String>),
53	/// Set the package support link metadata
54	SupportLink(Later<String>),
55	/// Set the package documentation metadata
56	Documentation(Later<String>),
57	/// Set the package source metadata
58	Source(Later<String>),
59	/// Set the package issues metadata
60	Issues(Later<String>),
61	/// Set the package community metadata
62	Community(Later<String>),
63	/// Set the package icon metadata
64	Icon(Later<String>),
65	/// Set the package banner metadata
66	Banner(Later<String>),
67	/// Set the package gallery metadata
68	Gallery(Vec<String>),
69	/// Set the package license metadata
70	License(Later<String>),
71	/// Set the package keywords metadata
72	Keywords(Vec<String>),
73	/// Set the package categories metadata
74	Categories(Vec<String>),
75	/// Set the package features property
76	Features(Vec<String>),
77	/// Set the package default features property
78	DefaultFeatures(Vec<String>),
79	/// Set the package content versions property
80	ContentVersions(Vec<String>),
81	/// Set the package Modrinth ID property
82	ModrinthID(Later<String>),
83	/// Set the package CurseForge ID property
84	CurseForgeID(Later<String>),
85	/// Set the package Smithed ID property
86	SmithedID(Later<String>),
87	/// Set the package supported versions property
88	SupportedVersions(Vec<VersionPattern>),
89	/// Set the package supported modloaders property
90	SupportedModloaders(Vec<ModloaderMatch>),
91	/// Set the package supported plugin loaders property
92	SupportedPluginLoaders(Vec<PluginLoaderMatch>),
93	/// Set the package supported sides property
94	SupportedSides(Vec<Side>),
95	/// Set the package supported operating systems property
96	SupportedOperatingSystems(Vec<OSCondition>),
97	/// Set the package supported architectures property
98	SupportedArchitectures(Vec<ArchCondition>),
99	/// Set the package tags property
100	Tags(Vec<String>),
101	/// Set the open source property
102	OpenSource(Later<bool>),
103	/// Install an addon
104	Addon {
105		/// The ID of the addon
106		id: Value,
107		/// The filename of the addon
108		file_name: Value,
109		/// What kind of addon this is
110		kind: Option<AddonKind>,
111		/// The URL to the addon file; may not exist
112		url: Value,
113		/// The path to the addon file; may not exist
114		path: Value,
115		/// The version of the addon
116		version: Value,
117		/// The addon's hashes
118		hashes: PackageAddonHashes<Value>,
119	},
120	/// Set a variable to a value
121	Set(Later<String>, Value),
122	/// Require a package
123	Require(Vec<Vec<super::parse::require::Package>>),
124	/// Refuse a package
125	Refuse(Value),
126	/// Recommend a package
127	Recommend(bool, Value),
128	/// Bundle a package
129	Bundle(Value),
130	/// Compat with a package
131	Compat(Value, Value),
132	/// Extend a package
133	Extend(Value),
134	/// Finish evaluation early
135	Finish(),
136	/// Fail evaluation
137	Fail(Option<FailReason>),
138	/// Present a notice to the user
139	Notice(Value),
140	/// Run a command
141	Cmd(Vec<Value>),
142	/// Call another routine
143	Call(Later<String>),
144	/// Custom implementation-specific instruction
145	Custom(Later<String>, Vec<String>),
146}
147
148/// A non-nested else / else if block connected to an if
149#[derive(Debug, Clone)]
150pub struct ElseBlock {
151	/// The block to run if this else succeeds
152	pub block: BlockId,
153	/// An additional condition that might need to be satisfied, used for else if.
154	pub condition: Option<Condition>,
155}
156
157impl Display for InstrKind {
158	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159		write!(
160			f,
161			"{}",
162			match self {
163				Self::If { .. } => "if",
164				Self::Name(..) => "name",
165				Self::Description(..) => "description",
166				Self::LongDescription(..) => "long_description",
167				Self::Authors(..) => "authors",
168				Self::PackageMaintainers(..) => "package_maintainers",
169				Self::Website(..) => "website",
170				Self::SupportLink(..) => "support_link",
171				Self::Documentation(..) => "documentation",
172				Self::Source(..) => "source",
173				Self::Issues(..) => "issues",
174				Self::Community(..) => "community",
175				Self::Icon(..) => "icon",
176				Self::Banner(..) => "banner",
177				Self::Gallery(..) => "gallery",
178				Self::License(..) => "license",
179				Self::Keywords(..) => "keywords",
180				Self::Categories(..) => "categories",
181				Self::Features(..) => "features",
182				Self::DefaultFeatures(..) => "default_features",
183				Self::ContentVersions(..) => "content_versions",
184				Self::ModrinthID(..) => "modrinth_id",
185				Self::CurseForgeID(..) => "curseforge_id",
186				Self::SmithedID(..) => "smithed_id",
187				Self::SupportedVersions(..) => "supported_versions",
188				Self::SupportedModloaders(..) => "supported_modloaders",
189				Self::SupportedPluginLoaders(..) => "supported_plugin_loaders",
190				Self::SupportedSides(..) => "supported_sides",
191				Self::SupportedOperatingSystems(..) => "supported_operating_systems",
192				Self::SupportedArchitectures(..) => "supported_architectures",
193				Self::Tags(..) => "tags",
194				Self::OpenSource(..) => "open_source",
195				Self::Addon { .. } => "addon",
196				Self::Set(..) => "set",
197				Self::Require(..) => "require",
198				Self::Refuse(..) => "refuse",
199				Self::Recommend(..) => "recommend",
200				Self::Bundle(..) => "bundle",
201				Self::Compat(..) => "compat",
202				Self::Extend(..) => "extend",
203				Self::Finish() => "finish",
204				Self::Fail(..) => "fail",
205				Self::Notice(..) => "notice",
206				Self::Cmd(..) => "cmd",
207				Self::Call(..) => "call",
208				Self::Custom(..) => "custom",
209			}
210		)
211	}
212}
213
214impl Instruction {
215	/// Create a new instruction
216	pub fn new(kind: InstrKind, pos: TextPos) -> Self {
217		Self { kind, pos }
218	}
219
220	/// Starts an instruction from the provided string
221	pub fn from_str(string: &str, pos: TextPos) -> anyhow::Result<Self> {
222		let kind = match string {
223			"name" => Ok::<InstrKind, anyhow::Error>(InstrKind::Name(Later::Empty)),
224			"description" => Ok(InstrKind::Description(Later::Empty)),
225			"long_description" => Ok(InstrKind::LongDescription(Later::Empty)),
226			"authors" => Ok(InstrKind::Authors(Vec::new())),
227			"package_maintainers" => Ok(InstrKind::PackageMaintainers(Vec::new())),
228			"website" => Ok(InstrKind::Website(Later::Empty)),
229			"support_link" => Ok(InstrKind::SupportLink(Later::Empty)),
230			"documentation" => Ok(InstrKind::Documentation(Later::Empty)),
231			"source" => Ok(InstrKind::Source(Later::Empty)),
232			"issues" => Ok(InstrKind::Issues(Later::Empty)),
233			"community" => Ok(InstrKind::Community(Later::Empty)),
234			"icon" => Ok(InstrKind::Icon(Later::Empty)),
235			"banner" => Ok(InstrKind::Banner(Later::Empty)),
236			"license" => Ok(InstrKind::License(Later::Empty)),
237			"keywords" => Ok(InstrKind::Keywords(Vec::new())),
238			"categories" => Ok(InstrKind::Categories(Vec::new())),
239			"features" => Ok(InstrKind::Features(Vec::new())),
240			"default_features" => Ok(InstrKind::DefaultFeatures(Vec::new())),
241			"content_versions" => Ok(InstrKind::ContentVersions(Vec::new())),
242			"modrinth_id" => Ok(InstrKind::ModrinthID(Later::Empty)),
243			"curseforge_id" => Ok(InstrKind::CurseForgeID(Later::Empty)),
244			"supported_versions" => Ok(InstrKind::SupportedVersions(Vec::new())),
245			"supported_modloaders" => Ok(InstrKind::SupportedModloaders(Vec::new())),
246			"supported_plugin_loaders" => Ok(InstrKind::SupportedPluginLoaders(Vec::new())),
247			"supported_sides" => Ok(InstrKind::SupportedSides(Vec::new())),
248			"supported_operating_systems" => Ok(InstrKind::SupportedOperatingSystems(Vec::new())),
249			"supported_architectures" => Ok(InstrKind::SupportedArchitectures(Vec::new())),
250			"tags" => Ok(InstrKind::Tags(Vec::new())),
251			"open_source" => Ok(InstrKind::OpenSource(Later::Empty)),
252			"set" => Ok(InstrKind::Set(Later::Empty, Value::None)),
253			"finish" => Ok(InstrKind::Finish()),
254			"fail" => Ok(InstrKind::Fail(None)),
255			"refuse" => Ok(InstrKind::Refuse(Value::None)),
256			"recommend" => Ok(InstrKind::Recommend(false, Value::None)),
257			"bundle" => Ok(InstrKind::Bundle(Value::None)),
258			"compat" => Ok(InstrKind::Compat(Value::None, Value::None)),
259			"extend" => Ok(InstrKind::Extend(Value::None)),
260			"notice" => Ok(InstrKind::Notice(Value::None)),
261			"call" => Ok(InstrKind::Call(Later::Empty)),
262			"custom" => Ok(InstrKind::Custom(Later::Empty, Vec::new())),
263			string => bail!("Unknown instruction '{string}' {}", pos),
264		}?;
265
266		Ok(Instruction::new(kind, pos))
267	}
268
269	/// Checks if this instruction is finished parsing
270	/// Only works for simple instructions. Will panic for instructions with special parse modes
271	pub fn is_finished_parsing(&self) -> bool {
272		match &self.kind {
273			InstrKind::Name(val)
274			| InstrKind::Description(val)
275			| InstrKind::LongDescription(val)
276			| InstrKind::SupportLink(val)
277			| InstrKind::Documentation(val)
278			| InstrKind::Source(val)
279			| InstrKind::Issues(val)
280			| InstrKind::Community(val)
281			| InstrKind::Icon(val)
282			| InstrKind::Banner(val)
283			| InstrKind::License(val)
284			| InstrKind::ModrinthID(val)
285			| InstrKind::CurseForgeID(val)
286			| InstrKind::SmithedID(val)
287			| InstrKind::Website(val)
288			| InstrKind::Call(val)
289			| InstrKind::Custom(val, _) => val.is_full(),
290			InstrKind::Features(val)
291			| InstrKind::Authors(val)
292			| InstrKind::PackageMaintainers(val)
293			| InstrKind::DefaultFeatures(val)
294			| InstrKind::ContentVersions(val)
295			| InstrKind::Keywords(val)
296			| InstrKind::Categories(val)
297			| InstrKind::Tags(val)
298			| InstrKind::Gallery(val) => !val.is_empty(),
299			InstrKind::Refuse(val)
300			| InstrKind::Recommend(_, val)
301			| InstrKind::Bundle(val)
302			| InstrKind::Extend(val)
303			| InstrKind::Notice(val) => val.is_some(),
304			InstrKind::SupportedVersions(val) => !val.is_empty(),
305			InstrKind::SupportedModloaders(val) => !val.is_empty(),
306			InstrKind::SupportedPluginLoaders(val) => !val.is_empty(),
307			InstrKind::SupportedSides(val) => !val.is_empty(),
308			InstrKind::SupportedOperatingSystems(val) => !val.is_empty(),
309			InstrKind::SupportedArchitectures(val) => !val.is_empty(),
310			InstrKind::OpenSource(val) => val.is_full(),
311			InstrKind::Compat(val1, val2) => val1.is_some() && val2.is_some(),
312			InstrKind::Set(var, val) => var.is_full() && val.is_some(),
313			InstrKind::Cmd(list) => !list.is_empty(),
314			InstrKind::Fail(..) | InstrKind::Finish() => true,
315			InstrKind::If { .. } | InstrKind::Addon { .. } | InstrKind::Require(..) => {
316				unimplemented!()
317			}
318		}
319	}
320
321	/// Parses a token and returns true if finished.
322	pub fn parse(&mut self, tok: &Token, pos: &TextPos) -> anyhow::Result<bool> {
323		if let Token::Semicolon = tok {
324			if !self.is_finished_parsing() {
325				bail!("Instruction was incomplete {pos}");
326			}
327			Ok(true)
328		} else {
329			match &mut self.kind {
330				InstrKind::Name(text)
331				| InstrKind::Description(text)
332				| InstrKind::LongDescription(text)
333				| InstrKind::Website(text)
334				| InstrKind::SupportLink(text)
335				| InstrKind::Documentation(text)
336				| InstrKind::Source(text)
337				| InstrKind::Issues(text)
338				| InstrKind::Community(text)
339				| InstrKind::Icon(text)
340				| InstrKind::Banner(text)
341				| InstrKind::License(text)
342				| InstrKind::ModrinthID(text)
343				| InstrKind::CurseForgeID(text) => {
344					if text.is_empty() {
345						text.fill(parse_string(tok, pos)?);
346					} else {
347						unexpected_token!(tok, pos);
348					}
349				}
350				InstrKind::Refuse(val)
351				| InstrKind::Bundle(val)
352				| InstrKind::Notice(val)
353				| InstrKind::Extend(val) => {
354					if let Value::None = val {
355						*val = parse_arg(tok, pos)?;
356					} else {
357						unexpected_token!(tok, pos);
358					}
359				}
360				InstrKind::Authors(list)
361				| InstrKind::PackageMaintainers(list)
362				| InstrKind::Features(list)
363				| InstrKind::DefaultFeatures(list)
364				| InstrKind::ContentVersions(list)
365				| InstrKind::Keywords(list)
366				| InstrKind::Categories(list)
367				| InstrKind::Tags(list)
368				| InstrKind::Gallery(list) => list.push(parse_string(tok, pos)?),
369				InstrKind::Cmd(list) => list.push(parse_arg(tok, pos)?),
370				InstrKind::Custom(cmd, args) => {
371					if cmd.is_empty() {
372						cmd.fill(parse_string(tok, pos)?);
373					} else {
374						args.push(parse_string(tok, pos)?);
375					}
376				}
377				InstrKind::Recommend(inverted, val) => match tok {
378					Token::Bang => {
379						if *inverted || val.is_some() {
380							unexpected_token!(tok, pos);
381						}
382
383						*inverted = true;
384					}
385					_ => {
386						if let Value::None = val {
387							*val = parse_arg(tok, pos)?;
388						} else {
389							unexpected_token!(tok, pos);
390						}
391					}
392				},
393				InstrKind::SupportedModloaders(list) => match tok {
394					Token::Ident(name) => {
395						if let Some(val) = ModloaderMatch::parse_from_str(name) {
396							list.push(val);
397						} else {
398							bail!("Value is not a valid modloader match argument")
399						}
400					}
401					_ => unexpected_token!(tok, pos),
402				},
403				InstrKind::SupportedPluginLoaders(list) => match tok {
404					Token::Ident(name) => {
405						if let Some(val) = PluginLoaderMatch::parse_from_str(name) {
406							list.push(val);
407						} else {
408							bail!("Value is not a valid plugin loader match argument")
409						}
410					}
411					_ => unexpected_token!(tok, pos),
412				},
413				InstrKind::SupportedSides(list) => match tok {
414					Token::Ident(name) => {
415						if let Some(val) = Side::parse_from_str(name) {
416							list.push(val);
417						} else {
418							bail!("Value is not a valid side argument")
419						}
420					}
421					_ => unexpected_token!(tok, pos),
422				},
423				InstrKind::OpenSource(val) => match tok {
424					Token::Ident(name) => match yes_no(name) {
425						Some(yes_no) => val.fill(yes_no),
426						None => bail!("Value is not a valid open_source argument"),
427					},
428					_ => unexpected_token!(tok, pos),
429				},
430				InstrKind::Compat(package, compat) => {
431					if let Value::None = package {
432						*package = parse_arg(tok, pos)?;
433					} else if let Value::None = compat {
434						*compat = parse_arg(tok, pos)?;
435					} else {
436						unexpected_token!(tok, pos);
437					}
438				}
439				InstrKind::Set(var, val) => {
440					if var.is_full() {
441						if let Value::None = val {
442							*val = parse_arg(tok, pos)?;
443						} else {
444							unexpected_token!(tok, pos);
445						}
446					} else {
447						match tok {
448							Token::Ident(name) => var.fill(name.clone()),
449							_ => unexpected_token!(tok, pos),
450						}
451					}
452				}
453				InstrKind::Fail(reason) => match tok {
454					Token::Ident(name) => {
455						if reason.is_none() {
456							*reason = match FailReason::from_string(name) {
457								Some(reason) => Some(reason),
458								None => {
459									bail!("Unknown fail reason '{}' {}", name.clone(), pos.clone());
460								}
461							}
462						} else {
463							unexpected_token!(tok, pos);
464						}
465					}
466					_ => unexpected_token!(tok, pos),
467				},
468				InstrKind::Call(routine) => {
469					match tok {
470						Token::Ident(name) => {
471							if crate::routine::is_reserved(name) {
472								bail!("Cannot use reserved routine name '{name}' in call instruction {}", pos.clone());
473							}
474							routine.fill(name.clone())
475						}
476						_ => unexpected_token!(tok, pos),
477					}
478				}
479				_ => {}
480			}
481
482			Ok(false)
483		}
484	}
485}
486
487impl Display for Instruction {
488	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489		write!(f, "{}", self.kind)
490	}
491}
492
493/// Parses a generic instruction argument with variable support
494pub fn parse_arg(tok: &Token, pos: &TextPos) -> anyhow::Result<Value> {
495	match tok {
496		Token::Variable(name) => Ok(Value::Var(name.to_string())),
497		Token::Str(text) => Ok(Value::Literal(text.clone())),
498		Token::Num(num) => Ok(Value::Literal(num.to_string())),
499		_ => unexpected_token!(tok, pos),
500	}
501}
502
503/// Parses a constant string argument
504pub fn parse_string(tok: &Token, pos: &TextPos) -> anyhow::Result<String> {
505	match tok {
506		Token::Str(text) => Ok(text.clone()),
507		_ => unexpected_token!(tok, pos),
508	}
509}