tinyscript 0.6.1

Tiny, C-like scripting language.
Documentation
// Copyright © 2025 Stephan Kunz
//! [`ValueParselet`] analyzes and handles value tokens like numbers.

use crate::{
	compilation::{
		Lexer, Parser,
		error::CompilationError,
		token::{Token, TokenKind},
	},
	execution::{Chunk, op_code::OpCode},
	scripting_value::ScriptingValue,
};

use super::PrefixParselet;

#[derive(Clone)]
pub struct ValueParselet;

impl PrefixParselet for ValueParselet {
	fn parse(
		&self,
		lexer: &mut Lexer,
		parser: &mut Parser,
		chunk: &mut Chunk,
		token: Token,
	) -> Result<(), CompilationError> {
		match token.kind {
			TokenKind::Enum => {
				let Some(value) = lexer.enums().get(&token.origin) else {
					return Err(CompilationError::EnumValNotFound {
						value: token.origin.into(),
						pos: token.line,
					});
				};
				let offset = chunk.add_constant(ScriptingValue::Int64(i64::from(*value)))?;
				parser.emit_bytes(OpCode::Constant as u8, offset, chunk);
				Ok(())
			}
			TokenKind::FloatNumber => {
				let double: f64 = match token.origin.parse() {
					Ok(n) => n,
					Err(_) => {
						return Err(CompilationError::ParseNumber {
							token: token.origin.into(),
							pos: token.line,
						});
					}
				};

				let offset = chunk.add_constant(ScriptingValue::Float64(double))?;
				parser.emit_bytes(OpCode::Constant as u8, offset, chunk);
				Ok(())
			}
			TokenKind::HexNumber => {
				// remove the '0x' before parsing
				let literal = token.origin.trim_start_matches("0x");
				let Ok(value) = i64::from_str_radix(literal, 16) else {
					return Err(CompilationError::ParseHex {
						token: literal.into(),
						pos: token.line,
					});
				};
				let offset = chunk.add_constant(ScriptingValue::Int64(value))?;
				parser.emit_bytes(OpCode::Constant as u8, offset, chunk);
				Ok(())
			}
			TokenKind::IntNumber => {
				let Ok(value) = token.origin.parse::<i64>() else {
					return Err(CompilationError::ParseInt {
						token: token.origin.into(),
						pos: token.line,
					});
				};
				let offset = chunk.add_constant(ScriptingValue::Int64(value))?;
				parser.emit_bytes(OpCode::Constant as u8, offset, chunk);
				Ok(())
			}
			TokenKind::String => {
				let offset = chunk.add_constant(ScriptingValue::String(token.origin))?;
				parser.emit_bytes(OpCode::Constant as u8, offset, chunk);
				Ok(())
			}
			_ => Err(CompilationError::Unreachable {
				file: file!().into(),
				line: line!(),
			}),
		}
	}
}

#[cfg(test)]
mod tests {
	use alloc::{
		collections::btree_map::BTreeMap,
		string::{String, ToString},
	};

	use super::*;

	// check, that the auto traits are available
	const fn is_normal<T: Sized + Send + Sync>() {}

	fn token(kind: TokenKind, origin: &str) -> Token {
		Token {
			origin: origin.to_string(),
			offset: 0,
			line: 1,
			kind,
		}
	}

	#[test]
	const fn normal_types() {
		is_normal::<&ValueParselet>();
		is_normal::<ValueParselet>();
	}

	#[test]
	fn parse_enum_missing_value_errors() {
		// Normally the lexer only tags an identifier as `Enum` when it is
		// present in the enums map, so the `EnumValNotFound` branch is
		// unreachable through the public `Runtime` API.
		let enums = BTreeMap::<String, i8>::new();
		let mut lexer = Lexer::new(&enums, "");
		let mut parser = Parser::new();
		let mut chunk = Chunk::default();
		let result = ValueParselet.parse(&mut lexer, &mut parser, &mut chunk, token(TokenKind::Enum, "MISSING"));
		assert!(matches!(result, Err(CompilationError::EnumValNotFound { .. })));
	}

	#[test]
	fn parse_float_unparseable_origin_errors() {
		// The lexer only produces `FloatNumber` tokens from `[0-9.]` spans,
		// which always parse as `f64`, so this error path is not reachable
		// through normal input. Craft a token directly to cover it.
		let enums = BTreeMap::<String, i8>::new();
		let mut lexer = Lexer::new(&enums, "");
		let mut parser = Parser::new();
		let mut chunk = Chunk::default();
		let result = ValueParselet.parse(&mut lexer, &mut parser, &mut chunk, token(TokenKind::FloatNumber, "not-a-float"));
		assert!(matches!(result, Err(CompilationError::ParseNumber { .. })));
	}

	#[test]
	fn parse_rejects_non_value_token() {
		// `ValueParselet` is only registered for number / string / enum
		// kinds; the final `_` arm is otherwise unreachable.
		let enums = BTreeMap::<String, i8>::new();
		let mut lexer = Lexer::new(&enums, "");
		let mut parser = Parser::new();
		let mut chunk = Chunk::default();
		let result = ValueParselet.parse(&mut lexer, &mut parser, &mut chunk, Token::none());
		assert!(matches!(result, Err(CompilationError::Unreachable { .. })));
	}
}