gruggers 0.4.1

rust implementation of the grug language
Documentation
// This is to ensure that any results that come 
// from parsing are not ignored
#![deny(unused_must_use)]
#![deny(unused_mut)]
// #![deny(warnings)]
use crate::error::GrugError;
use crate::state::GrugState;
// use crate::backend::GrugAst;
use crate::types::GrugFileId;
use crate::ast::*;
use crate::arena::Arena;
use crate::ntstring::NTStrPtr;

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

use std::sync::Arc;
// 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;

impl GrugState {
	// Path is relative to mods directory
	pub fn compile_grug_file(&self, path: &str) -> Result<GrugFileId, GrugError> {
		let mut path_buf = self.mods_dir_path.clone();

		path_buf.push('\\');

		path_buf.push_str(path);
		let file_text = std::fs::read_to_string(path_buf).unwrap();

		self.compile_grug_file_from_str(path, &file_text)
	}

	// Path is relative to mods directory or an absolute path
	pub fn compile_grug_file_from_str(&self, path: &str, file_text: &str) -> Result<GrugFileId, GrugError> {
		let mod_name = get_mod_name(path);
		let entity_type = get_entity_type(path)?;

		let mut arena = self.arenas.borrow_mut().pop().unwrap_or_else(|| Arena::new());
		// immediately invoked closure so we get try {} finally {}
		let id = (|| {
			let tokens = tokenizer::tokenize(file_text, &arena)?;

			let mut ast = parser::parse(&tokens, &arena)?;

			let entity = self.mod_api.entities().get(entity_type).ok_or_else(|| TypePropogatorError::EntityDoesNotExist{
				entity_name: Arc::from(entity_type),
			})?;
			let game_functions = self.mod_api.game_functions();
			
			TypePropogator::new(entity, game_functions, &self.game_functions, mod_name.into()).fill_result_types(entity_type, &mut ast, &arena)?;

			// let mod_api_entity = self.mod_api.entities.get(entity_type);
			let mut member_variables = Vec::new_in(&arena);
			let mut on_functions = Vec::new_in(&arena);
			on_functions.extend((0..entity.on_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.into()),
					GlobalStatement::OnFunction(st@OnFunction        {..}) => {
						let (i, _) = entity.get_on_fn(st.name.to_str()).unwrap();
						on_functions[i] = Some(&*Box::leak(Box::new_in(st.into(), &arena)));
					}
					GlobalStatement::HelperFunction(st@HelperFunction{..}) => helper_functions.push(st.into()),
					_ => (),
				}
			});

			let file = GrugAst{
				members: member_variables.leak(),
				on_functions: on_functions.leak(),
				helper_functions: helper_functions.leak(),
			};
			let mut path_to_script_ids = self.path_to_script_ids.borrow_mut();
			let id = match path_to_script_ids.get(path) {
				Some(id) => *id,
				None => {
					let id = self.get_next_script_id();
					assert!(path_to_script_ids.insert(String::from(path), id).is_none());
					id
				}
			};
			self.backend.insert_file(self, id, file);
			Ok(id)
		})();
		arena.clear();
		self.arenas.borrow_mut().push(arena);
		
		id
	}
}

// TODO: This should not be defined here, it should be defined within gruggers
/// 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,
}

fn get_mod_name (path: &str) -> &str {
	path.split_once('/').map(|x| x.0).unwrap_or(path)
	// 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)}))
}

fn get_entity_type(path: &str) -> Result<&str, FileError> {
	let (_, entity_type) = path.rsplit_once("-").ok_or(
			FileError::EntityMissing{path: String::from(path)}
		)?;
	let (entity_type, _) = entity_type.rsplit_once(".").ok_or(
			FileError::MissingPeriodInFileName{path: String::from(path)}
		)?;
	if entity_type.len() > MAX_FILE_ENTITY_TYPE_LENGTH {
		return Err(FileError::EntityLenExceedsMaxLen{path: String::from(path), entity_len: entity_type.len()});
	}
	if entity_type.is_empty() {
		return Err(FileError::EntityMissing{path: String::from(path)});
	}
	check_custom_id_is_pascal(entity_type)
}

fn check_custom_id_is_pascal(entity_type: &str) -> Result<&str, FileError> {
	let mut chars = entity_type.chars();
	let Some(_) = chars.next() else {
		return Err(FileError::EntityNotPascalCase1{entity_type: String::from(entity_type)});
	};
	for ch in chars {
		if !(ch.is_uppercase() || ch.is_lowercase() || ch.is_ascii_digit()) {
			return Err(FileError::EntityNotPascalCase2{entity_type: String::from(entity_type), wrong_char: ch});
		}
	}
	Ok(entity_type)
}

#[derive(Debug)]
pub enum FileError {
	FilePathDoesNotContainForwardSlash{
		path: String
	},
	MissingPeriodInFileName {
		path: String
	},
	EntityLenExceedsMaxLen {
		path: String,
		entity_len: usize,
	},
	EntityMissing {
		path: String
	},
	EntityNotPascalCase1 {
		entity_type: String,
	},
	EntityNotPascalCase2 {
		entity_type: String,
		wrong_char: char,
	}
}

impl std::fmt::Display for FileError {
	fn fmt (&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
		match self {
			Self::FilePathDoesNotContainForwardSlash{
				path
			} => write!(f, "The grug file path {}, does not contain a '/' character", path),
			Self::MissingPeriodInFileName {
				path
			} => write!(f, "'{}' is missing a period in its filename", path),
			Self::EntityLenExceedsMaxLen {
				path,
				entity_len: _,
			} => write!(f, 
				"There are more than {MAX_FILE_ENTITY_TYPE_LENGTH} characters \n\
				in the entity type of '{path}', exceeding MAX_FILE_ENTITY_TYPE_LENGTH"
			),
			Self::EntityMissing {
				path
			} => write!(f, 
				"'{}' is missing an entity type in its name;\n\
				use a dash to specify it, like 'ak47-gun.grug'",
				path
			),
			Self::EntityNotPascalCase1 {
				entity_type,
			} => write!(f, "'{entity_type}' seems like a custom ID type, but isn't in PascalCase"),
			Self::EntityNotPascalCase2 {
				entity_type,
				wrong_char,
			} => write!(f,
				"'{entity_type}' seems like a custom ID type, but it contains '{wrong_char}', \n\
				which isn't uppercase/lowercase/a digit"
			),
		}
	}
}

pub mod type_propagation;
use type_propagation::*;