gruggers 0.9.0

rust implementation of the grug language
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
use crate::state::{GrugState, Files, FileInfo};
use crate::arena::Arena;
use crate::types::FileId;
use crate::ast::*;
use crate::ntstring::{NTStrPtr, NTStr, NTBytes};
use crate::error::{Error, ErrorKind, SourceSpan};
use crate::mod_api::ModApi;
use crate::own_ptr::OwnPtr;
use crate::type_storage::TypeStorage;

use allocator_api2::vec::Vec;
use allocator_api2::boxed::Box as Box2;

use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::path::Path;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender};
// use std::path::Path;

const MAX_FILE_ENTITY_TYPE_LENGTH: usize = 420;
pub(crate) const SPACES_PER_INDENT: usize = 4;

pub mod tokenizer;
pub mod parser;
pub mod type_propagation;
use type_propagation::TypePropagator;

// Compilation functions
impl GrugState {
	/// Send at most this many files to a thread for compilation
	const FILES_PER_THREAD: usize = 8;
	// All 'static fields are actually allocated within the arena
	/// Uses async file system apis on windows
	pub(crate) fn compiler_thread_fn(
		// these paths are relative
		receiver: Receiver<(Arena, &'static [&'static OsStr])>,
		sender: Sender<(
			Arena, 
			OwnPtr<'static, [(
				Result<GrugAst<'static>, Error>, 
				// derived from input paths
				// These paths are relative
				&'static OsStr
			)]>, 
			// Resources
			// These paths are relative
			&'static [&'static OsStr]
		)>,
		// path is absolute
		mods_dir_path: PathBuf,
		mod_api: Arc<ModApi>,
	) -> impl FnOnce() {
		use crate::async_fs::{open_file_async_for_read, read_files_async};
		move || {
			let mut temp_arena = Arena::new();
			let mut type_storage = TypeStorage::new();
			
			for (arena, files) in receiver.iter() {
				temp_arena.clear();
				let mut resources = Vec::new_in(&arena);
				// This is the actual lifetime of the data but it has to be erased to send across the channel
				fn combine_lifetimes<'a>(_: &'a Arena, input: &'static [&'static OsStr]) -> &'a [&'a OsStr] {input}
				let files = combine_lifetimes(&arena, files);

				let mut results = Vec::new_in(&arena);
				// read all files
				// split into files that can be opened and files that can't
				let mut ok_files = Vec::new_in(&arena);
				for file_path in files.iter().copied() {
					let mut abs_path = mods_dir_path.clone();
					abs_path.push(file_path);
					match open_file_async_for_read(&abs_path) {
						Ok(file) => {
							ok_files.push((file, file_path)); 
						},
						Err(err) => {
							results.push((Err(err), file_path));
						}
					}
				}
				// read the contents of files that can be read
				let ok_files_data = read_files_async(ok_files.iter().map(|(file, path)| (file, *path)), &arena);
				// compile files one by one and collect errors, asts and resources
				results.extend(ok_files_data.into_iter().zip(&ok_files).map(|(data, (_, path))| {
					let path = *path;
					let file_text = match data {
						Ok(data) => data,
						Err(err) => return (Err(err), path),
					};

					// compile the file
					let (ast, current_resources) = match Self::compile_inner(
						path,
						file_text,
						mods_dir_path.as_ref(),
						&mod_api,
						&arena,
						&temp_arena,
						&mut type_storage,
					) {
						Ok(data) => data,
						Err(err) => return (Err(err), path)
					};
					// add resource paths 
					resources.extend_from_slice(current_resources);
					// collect errors and resources
					(Ok(ast), path)
				}));
				drop(ok_files);


				let results = unsafe{std::mem::transmute::<
					OwnPtr<[(
						Result<
							GrugAst<'_>,
							Error
						>, 
						&OsStr
					)]>, 
					OwnPtr<'static, [(
						Result<
							GrugAst<'static>,
							Error
						>, 
						&'static OsStr
					)]>
				>(results.into_boxed_slice().into())};
				let resources = unsafe{std::mem::transmute::<&[&OsStr], &'static[&'static OsStr]>(resources.leak())};
				let Ok(()) = sender.send((arena, results, resources)) else {break;};
			}
		}
	}

	/// Compile a grug file at a relative path within the mods directory. Once
	/// compiled directly once, the file will be automatically hot reloaded by
	/// the state. 
	pub fn compile_grug_file(&self, path: impl AsRef<OsStr>) -> Result<FileId, Error> {
		let path = path.as_ref();
		let mut path_buf = self.mods_dir_path.clone();
		path_buf.push("/");
		path_buf.push(path);
		
		let file_text = match std::fs::read_to_string(path_buf) {
			Ok(file_text) => file_text,
			Err(err) => return Err(Error::from_io_error(err, path))
		};

		self.compile_grug_file_from_str(path, &file_text)
	}

	/// Compile a grug file directly from a string. The path is required to
	/// identify the entity type and to uniquely identify a particular script
	/// for hot reloading.
	///
	/// If the path is the same as a path within the mods directory, when the
	/// actual file changes, the script will be hot reloaded. 
	///
	/// If a file has already been compiled with the same path, the script will
	/// be hot reloaded.
	pub fn compile_grug_file_from_str(&self, path: impl AsRef<OsStr>, file_text: &str) -> Result<FileId, Error> {
		use super::frontend::*;
		use crate::async_fs::{verify_file_data};
		let path = path.as_ref();

		let mut arena = self.arenas.borrow_mut().pop().unwrap_or_default();
		let file_text = arena.copy_bytes_into_nt(file_text.as_bytes());
		let file_text = verify_file_data(file_text, path)?;
		// immediately invoked closure so we get try {} finally {}
		let id = (|| {
			let (file, resources) = Self::compile_inner(
				path, 
				file_text, 
				&self.mods_dir_path, 
				&self.mod_api, 
				&arena,
				&arena,
				&mut *self.type_storage.borrow_mut(),
			)?;
			let mut self_resources = self.resources.borrow_mut();
			for resource in resources {
				if !self_resources.contains(*resource) {self_resources.insert(OsString::from(resource));}
			}
			let id = self.get_or_insert_script_id(path.as_ref());
			self.backend.insert_file(id, &file);

			let mut script_entities = self.script_entities.borrow_mut();
			// Add entity tracking info to self.script_entities
			if (id.to_inner() as usize) < script_entities.len() {
				for entity in &script_entities[id.to_inner() as usize] {
					self.backend.init_entity(self, unsafe{&*entity.as_ptr()});
				}
			} else if id.to_inner() as usize == script_entities.len() {
				script_entities.push(std::vec::Vec::new());
			} else {
				unreachable!();
			}

			Ok(id)
		})();
		arena.clear();
		self.arenas.borrow_mut().push(arena);
		
		id
	}

	/// Compile all the files within the mods directory. Uses asynchronous file
	/// system apis if available on the current platform
	pub fn compile_all_files(&self) -> Files {
		// iterate over all files and get all valid paths
		let arena = self.arenas.borrow_mut().pop().unwrap_or_else(Arena::new);

		let mut file_paths = Vec::new_in(&arena);
		let mut files = std::vec::Vec::new();

		let mods_dir_len = if self.mods_dir_path.as_encoded_bytes().last().is_some_and(|x| *x != b'\\' && *x != b'/') {self.mods_dir_path.len() + 1} else {self.mods_dir_path.len()};
		// Iterate through every directory within the mods directory and
		// collect relative paths to all grug scripts
		for mod_dir in std::fs::read_dir(&self.mods_dir_path).expect("Could not read mods directory") {
			let Ok(mod_dir) = mod_dir else {
				panic!("unable to read directory: {:?}", mod_dir);
			};
			let mut entries_to_check = std::vec::Vec::from([mod_dir]);

			while let Some(next_entry) = entries_to_check.pop() {
				if next_entry.metadata().expect("could not read metadata").is_dir() {
					let next_entry_path = next_entry.path();
					for entry in std::fs::read_dir(&next_entry_path).expect("Could not read mods directory") {
						let Ok(entry) = entry else {
							panic!("unable to read entry: {:?}", entry);
						};
						entries_to_check.push(entry);
					}
				} else {
					let entry_path = next_entry.path();
					if let Some(extension) = entry_path.extension() && extension == "grug" {
						// get path relative to mods dir
						let rel_path = unsafe{OsStr::from_encoded_bytes_unchecked(&entry_path.as_os_str().as_encoded_bytes()[mods_dir_len..])};
						// copy path into arena
						let rel_path = arena.copy_osstr_into(rel_path);
						file_paths.push(rel_path);
					};
				}
			}
		}

		let mut next_thread = self.compiler_senders.iter().cycle();
		let sent_count = file_paths.len();
		let mut recv_count = 0;
		// Chunk into FILES_PER_THREAD sized blocks
		for chunk in file_paths.chunks(Self::FILES_PER_THREAD) {
			let cur_arena = self.arenas.borrow_mut().pop().unwrap_or_else(Arena::new);

			// We need to allocate the paths into the new arena to ensure panic safety
			//
			// If these strings were allocated in the outer arena (or if
			// they are a reference to the existing PathBufs), then if this
			// thread panics for any reason, the strings will be freed and
			// the compiler threads will access freed memory.
			let chunk = cur_arena.slice_from_iter(chunk.iter().map(|item| cur_arena.copy_osstr_into(item.as_ref())));
			// SAFETY: make sure we never use this slice outside the current loop iteration
			let chunk = unsafe{std::mem::transmute::<&[&OsStr], &'static [&'static OsStr]>(chunk)};
			// Send to threads
			next_thread.next().expect("at least one compiler thread").send((cur_arena, chunk)).expect("send succeeds");
		}

		// Send to backend while recieving
		while recv_count < sent_count {
			let (mut current_arena, results, resources) = self.compiler_receiver.recv().unwrap(); 
			recv_count += results.len();

			for (result, path) in results {
				// turn ok result into id, allocate error into outer arena
				let result = match result {
					Ok(ast) => {
						let id = self.get_or_insert_script_id(path.as_ref());
						// Send to backend
						self.backend.insert_file(id, &ast);

						let mut script_entities = self.script_entities.borrow_mut();
						// Add entity tracking info to self.script_entities
						if (id.to_inner() as usize) < script_entities.len() {
							for entity in &script_entities[id.to_inner() as usize] {
								self.backend.init_entity(self, unsafe{&*entity.as_ptr()});
							}
						} else if id.to_inner() as usize == script_entities.len() {
							script_entities.push(std::vec::Vec::new());
						} else {
							unreachable!();
						}
						Ok(id)
					}
					Err(err) => {
						let err = err.inner().copy_into(&arena);
						Err(err)
					}
				};
				// Create FileInfo from this result
				let path = <OsStr as AsRef<Path>>::as_ref(path);
				let mod_dir_path = path.parent().expect("must have at least component in path").components().next().unwrap().as_os_str();
				let info = FileInfo::new_in(
					path.as_os_str(),
					path.file_name().unwrap(),
					mod_dir_path,
					get_entity_type(path.as_os_str()).unwrap_or(""),
					path.file_prefix().unwrap(),
					result,
					&arena
				);
				files.push(info);
			}
			let mut self_resources = self.resources.borrow_mut();
			for resource in resources {
				if !self_resources.contains(*resource) {
					self_resources.insert(OsString::from(resource));
				}
			}
			// `results` and `resources` are allocated within current_arena, so it
			// is only safe to clear the current_arena now.
			current_arena.clear();
			self.arenas.borrow_mut().push(current_arena);
		}
		drop(file_paths);
		Files {
			inner: unsafe{std::mem::transmute::<OwnPtr<[FileInfo]>, OwnPtr<'static, [FileInfo]>>(files.into_boxed_slice().into())},
			_arena: arena,
		}
	}

	/// Check if there are any files in the mods directory that need to be hot reloaded. 
	/// Also returns any resources that need to be reloaded
	pub fn update_files(&self) -> (std::vec::Vec<OsString>, Files) {
		let arena = self.arenas.borrow_mut().pop().unwrap_or_else(Arena::new);
		let mut file_paths = Vec::new_in(&arena);
		let mut updated_resources = std::vec::Vec::new();
		
		let mut grug_files = std::vec::Vec::new();
		for change in self.changes.try_iter() {
			let file_name = change.expect("File IO error");
			if let Some(extension) = <OsStr as AsRef<Path>>::as_ref(&file_name).extension() && extension == "grug" {
				if !file_paths.contains(&&*file_name) {
					let rel_name = arena.copy_osstr_into(file_name.as_ref());
					file_paths.push(rel_name);
				}
			}
			if self.resources.borrow().contains(&file_name) {
				if !updated_resources.contains(&file_name) {
					updated_resources.push(file_name);
				}
			}
		}

		let mut next_thread = self.compiler_senders.iter().cycle();
		let sent_count = file_paths.len();
		let mut recv_count = 0;
		// Chunk into FILES_PER_THREAD sized blocks
		for chunk in file_paths.chunks(Self::FILES_PER_THREAD) {
			let cur_arena = self.arenas.borrow_mut().pop().unwrap_or_else(Arena::new);

			// We need to allocate the paths into the new arena to ensure panic safety
			//
			// If these strings were allocated in the outer arena (or if
			// they are a reference to the existing PathBufs), then if this
			// thread panics for any reason, the strings will be freed and
			// the compiler threads will access freed memory.
			let chunk = cur_arena.slice_from_iter(chunk.iter().map(|item| cur_arena.copy_osstr_into(item.as_ref())));
			// SAFETY: make sure we never use this slice outside the current loop iteration
			let chunk = unsafe{std::mem::transmute::<&[&OsStr], &'static [&'static OsStr]>(chunk)};
			// Send to threads
			next_thread.next().expect("at least one compiler thread").send((cur_arena, chunk)).expect("send succeeds");
		}

		// Send to backend while recieving
		while recv_count < sent_count {
			let (mut current_arena, results, resources) = self.compiler_receiver.recv().unwrap(); 
			recv_count += results.len();

			for (result, path) in results {
				// turn ok result into id, copy err into current arena
				let result = match result {
					Ok(ast) => {
						let id = self.get_or_insert_script_id(path.as_ref());
						// Send to backend
						self.backend.insert_file(id, &ast);

						let mut script_entities = self.script_entities.borrow_mut();
						// Add entity tracking info to self.script_entities
						// Script already exists, reload all files
						if (id.to_inner() as usize) < script_entities.len() {
							for entity in &script_entities[id.to_inner() as usize] {
								self.backend.init_entity(self, unsafe{&*entity.as_ptr()});
							}
						} else if id.to_inner() as usize == script_entities.len() {
							script_entities.push(std::vec::Vec::new());
						} else {
							unreachable!();
						}
						Ok(id)
					}
					Err(err) => {
						let err = err.inner().copy_into(&arena);
						Err(err)
					}
				};
				// Create FileInfo from this result
				let path = <OsStr as AsRef<Path>>::as_ref(path);
				let mod_dir_path = path.parent().expect("must have at least one component in path").components().next().unwrap().as_os_str();
				let info = FileInfo::new_in(
					path.as_os_str(),
					path.file_name().unwrap(),
					mod_dir_path,
					get_entity_type(path.as_os_str()).unwrap_or(""),
					path.file_prefix().unwrap(),
					result,
					&arena
				);
				grug_files.push(info);
			}
			let mut self_resources = self.resources.borrow_mut();
			for resource in resources {
				if !self_resources.contains(*resource) {
					self_resources.insert(OsString::from(resource));
				}
			}
			// `results` and `resources` are allocated within arena, so it
			// is only safe to clear the arena now.
			current_arena.clear();
			self.arenas.borrow_mut().push(current_arena);
		}
		drop(file_paths);
		let grug_files = Files {
			inner: unsafe{std::mem::transmute::<OwnPtr<[FileInfo]>, OwnPtr<'static, [FileInfo]>>(grug_files.into_boxed_slice().into())},
			_arena: arena,
		};
		(updated_resources, grug_files)
	}

	/// Merge threaded compilation and standalone compilation
	fn compile_inner<'arena>(
		path: &'arena OsStr, 
		file_text: &'arena NTStr, 
		mods_dir_path: &'arena OsStr, 
		mod_api: &'arena ModApi, 
		arena: &'arena Arena,
		temp_arena: &'_ Arena,
		type_storage: &mut TypeStorage,
	) -> Result<(GrugAst<'arena>, &'arena [&'arena OsStr]), Error> {
		let mod_name = get_mod_name(path);
		let entity_type = get_entity_type(path)?;

		if file_text.len() == 0 {
			return Err(Error::new(
				ErrorKind::EMPTY_FILE,
				"",
				path,
				"",
				SourceSpan {offset: 0, line: 1},
				format_args!("File is empty")
			));
		}

		// tokenize
		let tokens = tokenizer::tokenize(file_text, arena, path)?;
		// parse
		let ast = parser::parse(tokens.leak(), arena, file_text, path)?;

		// get mod api entity declaration
		let entity = mod_api.entities().get(entity_type).ok_or_else(|| 
			// TODO: This is not handled by grug_tests
			Error::new(
				ErrorKind::FILE_NAME_ERROR,
				"",
				path, 
				"",
				SourceSpan{offset: 0, line: 0},
				format_args!("Entity '{}' is not registered in the mod_api.json", entity_type),
			)

		)?;

		// type check 
		let (ast, resources) = TypePropagator::fill_result_types(
			entity, 
			mod_api,
			mod_name, 
			mods_dir_path, 
			file_text, 
			path,
			entity_type,
			ast,
			arena,
			temp_arena,
			type_storage,
		)?;

		// convert into GrugAst
		let mut member_variables = Vec::new_in(arena);
		let mut on_functions = Vec::new_in(arena);
		on_functions.extend((0..entity.export_fns.len()).map(|_| None));
		let mut helper_functions = Vec::new_in(arena);

		ast.global_statements.into_iter().for_each(|statement| {
			match statement {
				GlobalStatement::Variable(st@MemberVariable      {..}) => member_variables.push(st),
				GlobalStatement::OnFunction(st@OnFunction        {..}) => {
					let (i, _) = entity.get_export_fn(st.name.to_str()).unwrap();
					on_functions[i] = Some(&*Box2::leak(Box2::new_in(st, arena)));
				}
				GlobalStatement::HelperFunction(st@HelperFunction{..}) => helper_functions.push(st),
				_ => (),
			}
		});

		// SAFETY: copy_bytes_into_nt ensures the slice has a single null byte
		// at the end of the string
		let file_path = unsafe{NTBytes::from_bytes_unchecked(arena.copy_bytes_into_nt(path.as_encoded_bytes()))};
		let file = GrugAst{
			members: member_variables.leak(),
			on_functions: on_functions.leak(),
			helper_functions: helper_functions.leak(),
			file_text: file_text.as_ntstrptr(),
			file_path
		};
		Ok((file, resources))
	}
}

/// A top level statement in a grug file.
///
/// This is not passed through [`GrugAst`] but is instead supposed to be used
/// internally by a grug state implementation
#[derive(Debug)]
pub(crate) enum GlobalStatement<'a> {
	/// A member variable
	/// `x: number = 25`
	Variable(MemberVariable<'a>),
	/// An on function declaration
	/// ```text
	/// on_init(id: number) {
	///     set_max_health(50)
	///     set_unarmed_damage(2)
	///     set_weapon("sword.json")
	/// }
	/// ```
	OnFunction(OnFunction<'a>),
	/// A helper function declaration
	/// ```text
	/// helper_color(n: number) Color {
	///     if n == 0 {
	///         return color("blue")
	///     } else if n == 1 {
	///         return color("red")
	///     } else if n == 2 {
	///         return color("green")
	///     } else if n == 3 {
	///         return color("yellow")
	///     } else if n == 3 {
	///         return color("black")
	///     } 
	///     return game_fn_error("invalid color id")
	/// }
	/// ```
	HelperFunction(HelperFunction<'a>),
	/// A comment at the top level of a file
	/// ```text
	/// ## This is a global comment
	/// shared_number: number = 0
	/// ```
	Comment{
		value: NTStrPtr<'a>,
	},
	/// An Empty line at the top level of a script
	EmptyLine,
}

/// The mods directory contains a directory for each mod. Each mod may contain
/// several scripts. This function returns the name of the current mod
/// directory from the full path.
fn get_mod_name (path: &OsStr) -> &OsStr {
	let path = path.as_encoded_bytes();
	let mut slash_len = 0;
	for (i, ch) in path.iter().enumerate() {
		if *ch == b'/' || *ch == b'\\' {slash_len = i; break;}
	}
	// SAFETY: Next byte is b'/' which is valid utf8 or the length is 0.
	unsafe{OsStr::from_encoded_bytes_unchecked(&path[..slash_len])}
	
	// This restrict isn't checked in grug_tests and it gets in the way of
	// implementing the compiler in the simplest way
	// path.split_once('/').map(|x| x.0).ok_or(GrugError::FileError(FileError::FilePathDoesNotContainForwardSlash{path: String::from(path)}))
}

/// The filename of a grug script should be as follows <name>-<entity>.grug
///
/// Where <name> can be any utf8 string, and <entity> refers to the entity type
/// the file contains.
///
/// This function returns <entity> for a given file path (if <entity> exists)
fn get_entity_type(path: &OsStr) -> Result<&str, Error> {
	let mut dot_pos = None;
	let mut dash_pos = None;
	let path_bytes = path.as_encoded_bytes();
	let file_name = <OsStr as AsRef<Path>>::as_ref(path).file_name().unwrap_or("".as_ref());
	for (i, ch) in path_bytes.iter().enumerate().rev() {
		match (ch, dot_pos, dash_pos) {
			(b'.', None, None) => dot_pos = Some(i),
			(b'-', None, None) => 
				return Err(Error::new(
					ErrorKind::FILE_NAME_ERROR,
					"",
					path, 
					"",
					SourceSpan{offset: 0, line: 0},
					format_args!("'{}' is missing a period in its name", file_name.display())
				)),
			(b'-', Some(_), None) => {dash_pos = Some(i); break;},
			_ => (),
		}
	}
	let (dot_pos, dash_pos) = match (dot_pos, dash_pos) {
		(Some(dot_pos), Some(dash_pos)) if dot_pos == dash_pos + 1 => {
			return Err(Error::new(
				ErrorKind::FILE_NAME_ERROR,
				"",
				path, 
				"",
				SourceSpan{offset: 0, line: 0},
				format_args!("'{}' is missing an entity type in its name", file_name.display())
			));
		}
		(Some(dot_pos), Some(dash_pos)) => (dot_pos, dash_pos),
		_ => {
			return Err(Error::new(
				ErrorKind::FILE_NAME_ERROR,
				"",
				path, 
				"",
				SourceSpan{offset: 0, line: 0},
				format_args!("'{}' is missing an entity type in its name", file_name.display())
			));
		}
	};
	// SAFETY: dash_pos is b'-' which is valid utf8, and dot_pos is b'.' which
	// is also utf8 so (dash_pos+1)..dot_pos will not truncate a utf8 codepoint
	let entity_type = unsafe{OsStr::from_encoded_bytes_unchecked(&path_bytes[(dash_pos + 1)..dot_pos])};
	if entity_type.len() > MAX_FILE_ENTITY_TYPE_LENGTH {
		return Err(Error::new(
			ErrorKind::FILE_NAME_ERROR,
			"",
			path, 
			"",
			SourceSpan{offset: 0, line: 0},
			format_args!("There are more than {} characters \n\
				in the entity type of '{}', exceeding MAX_FILE_ENTITY_TYPE_LENGTH", 
				entity_type.len(), path.display()
			)
		));
	}
	check_custom_id_is_pascal(entity_type, path)
}

/// Entities and Id types in grug must be pascalCase.
fn check_custom_id_is_pascal<'a>(entity_type: &'a OsStr, path: &'_ OsStr) -> Result<&'a str, Error> {
	let entity_type = entity_type.to_str().ok_or_else(|| 
		Error::new(
			ErrorKind::FILE_NAME_ERROR,
			"",
			entity_type, 
			"",
			SourceSpan{offset: 0, line: 0},
			format_args!("'{}' is not valid utf8", 
				entity_type.display()
			)
		)

	)?;
	let mut chars = entity_type.chars();
	if let Some(first) = chars.next() && !first.is_uppercase() {
		return Err(Error::new(
			ErrorKind::FILE_NAME_ERROR,
			"",
			path, 
			"",
			SourceSpan{offset: 0, line: 0},
			format_args!("'{entity_type}' seems like a custom ID type, but it doesn't start in Uppercase")
		));
	}
	for ch in chars {
		if !(ch.is_uppercase() || ch.is_lowercase() || ch.is_ascii_digit()) {
			return Err(Error::new(
				ErrorKind::FILE_NAME_ERROR,
				"",
				path, 
				"",
				SourceSpan{offset: 0, line: 0},
				format_args!("'{entity_type}' seems like a custom ID type, but it contains '{ch}', which isn't uppercase, lowercase, or a digit", )
			));
		}
	}
	Ok(entity_type)
}