diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index fff5c75f..0aef60bb 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -52,8 +52,10 @@ jobs: - name: Build package run: | magic run mojo package src/decimojo + magic run mojo package src/tomlmojo cp decimojo.mojopkg tests/ cp decimojo.mojopkg benches/ + mv tomlmojo.mojopkg tests/ - name: Run tests run: | diff --git a/README.md b/README.md index e03668c8..08a8696b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ The core types are: The library is expanding to include `BigDecimal` types that support arbitrary precision[^arbitrary], allowing for calculations with unlimited digits and decimal places. These extensions are currently under active development. +This repository includes [TOMLMojo](https://github.com/forfudan/decimojo/tree/main/src/tomlmojo), a lightweight TOML parser in pure Mojo. It parses configuration files and test data, supporting basic types, arrays, and nested tables. While created for DeciMojo's testing framework, it offers general-purpose structured data parsing with a clean, simple API. + ## Installation DeciMojo is available in the [modular-community](https://repo.prefix.dev/modular-community) package repository. You can install it using any of these methods: diff --git a/mojoproject.toml b/mojoproject.toml index bd7f0585..7ad12688 100644 --- a/mojoproject.toml +++ b/mojoproject.toml @@ -17,11 +17,14 @@ max = ">=25.2" format = "magic run mojo format ./" # compile the package -package = "magic run format && magic run mojo package src/decimojo && cp decimojo.mojopkg tests/ && cp decimojo.mojopkg benches/ && rm decimojo.mojopkg" +package_decimojo = "magic run mojo package src/decimojo && cp decimojo.mojopkg tests/ && cp decimojo.mojopkg benches/ && rm decimojo.mojopkg" +package_tomlmojo = "magic run mojo package src/tomlmojo && mv tomlmojo.mojopkg tests/" +package = "magic run format && magic run package_decimojo && magic run package_tomlmojo" p = "clear && magic run package" # clean the package files in tests folder -clean = "rm tests/decimojo.mojopkg && rm benches/decimojo.mojopkg" +clean = "rm tests/decimojo.mojopkg && rm benches/decimojo.mojopkg && rm tests/tomlmojo.mojopkg" +c = "clear && magic run clean" # tests (use the mojo testing tool) test = "magic run package && magic run mojo test tests --filter" diff --git a/src/decimojo/__init__.mojo b/src/decimojo/__init__.mojo index 817da4ad..de599c23 100644 --- a/src/decimojo/__init__.mojo +++ b/src/decimojo/__init__.mojo @@ -1,7 +1,4 @@ # ===----------------------------------------------------------------------=== # -# DeciMojo: A fixed-point decimal arithmetic library in Mojo -# https://github.com/forfudan/decimojo -# # Copyright 2025 Yuhao Zhu # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/decimojo/tests.mojo b/src/decimojo/tests.mojo new file mode 100644 index 00000000..19ffd439 --- /dev/null +++ b/src/decimojo/tests.mojo @@ -0,0 +1,55 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +""" +Implement structs and functions for tests. +""" + + +struct TestCase: + """Structure to hold test case data. + + Attributes: + a: The first input value as numeric string. + b: The second input value as numeric string. + expected: The expected output value as numeric string. + description: A description of the test case. + """ + + var a: String + var b: String + var expected: String + var description: String + + fn __init__( + out self, a: String, b: String, expected: String, description: String + ): + self.a = a + self.b = b + self.expected = expected + self.description = description + "\nx1 = " + self.a + "\nx2 = " + self.b + + fn __copyinit__(out self, other: Self): + self.a = other.a + self.b = other.b + self.expected = other.expected + self.description = other.description + + fn __moveinit__(out self, owned other: Self): + self.a = other.a^ + self.b = other.b^ + self.expected = other.expected^ + self.description = other.description^ diff --git a/src/tomlmojo/__init__.mojo b/src/tomlmojo/__init__.mojo new file mode 100644 index 00000000..ed64b3af --- /dev/null +++ b/src/tomlmojo/__init__.mojo @@ -0,0 +1,21 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +""" +A simple TOML parser for Mojo. +""" + +from .parser import parse_file, TOMLValueType diff --git a/src/tomlmojo/parser.mojo b/src/tomlmojo/parser.mojo new file mode 100644 index 00000000..b4edf61f --- /dev/null +++ b/src/tomlmojo/parser.mojo @@ -0,0 +1,482 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +""" +A simple TOML parser for Mojo. +This provides basic parsing for TOML files, focusing on the core elements +needed for test case parsing. +""" + +from collections import Dict +from .tokenizer import Token, TokenType, Tokenizer + + +@value +struct TOMLValue: + """Represents a value in the TOML document.""" + + var type: TOMLValueType + var string_value: String + var int_value: Int + var float_value: Float64 + var bool_value: Bool + var array_values: List[TOMLValue] + var table_values: Dict[String, TOMLValue] + + fn __init__(out self): + """Initialize an empty TOML value.""" + self.type = TOMLValueType.NULL + self.string_value = "" + self.int_value = 0 + self.float_value = 0.0 + self.bool_value = False + self.array_values = List[TOMLValue]() + self.table_values = Dict[String, TOMLValue]() + + fn __init__(out self, string_value: String): + """Initialize a string TOML value.""" + self.type = TOMLValueType.STRING + self.string_value = string_value + self.int_value = 0 + self.float_value = 0.0 + self.bool_value = False + self.array_values = List[TOMLValue]() + self.table_values = Dict[String, TOMLValue]() + + fn __init__(out self, int_value: Int): + """Initialize an integer TOML value.""" + self.type = TOMLValueType.INTEGER + self.string_value = "" + self.int_value = int_value + self.float_value = 0.0 + self.bool_value = False + self.array_values = List[TOMLValue]() + self.table_values = Dict[String, TOMLValue]() + + fn __init__(out self, float_value: Float64): + """Initialize a float TOML value.""" + self.type = TOMLValueType.FLOAT + self.string_value = "" + self.int_value = 0 + self.float_value = float_value + self.bool_value = False + self.array_values = List[TOMLValue]() + self.table_values = Dict[String, TOMLValue]() + + fn __init__(out self, bool_value: Bool): + """Initialize a boolean TOML value.""" + self.type = TOMLValueType.BOOLEAN + self.string_value = "" + self.int_value = 0 + self.float_value = 0.0 + self.bool_value = bool_value + self.array_values = List[TOMLValue]() + self.table_values = Dict[String, TOMLValue]() + + fn as_string(self) -> String: + """Get the value as a string.""" + if self.type == TOMLValueType.STRING: + return self.string_value + elif self.type == TOMLValueType.INTEGER: + return String(self.int_value) + elif self.type == TOMLValueType.FLOAT: + return String(self.float_value) + elif self.type == TOMLValueType.BOOLEAN: + return "true" if self.bool_value else "false" + else: + return "" + + fn as_int(self) -> Int: + """Get the value as an integer.""" + if self.type == TOMLValueType.INTEGER: + return self.int_value + else: + return 0 + + fn as_float(self) -> Float64: + """Get the value as a float.""" + if self.type == TOMLValueType.FLOAT: + return self.float_value + elif self.type == TOMLValueType.INTEGER: + return Float64(self.int_value) + else: + return 0.0 + + fn as_bool(self) -> Bool: + """Get the value as a boolean.""" + if self.type == TOMLValueType.BOOLEAN: + return self.bool_value + else: + return False + + +@value +struct TOMLValueType: + """Types of values in TOML.""" + + # Aliases to mimic enum constants + alias NULL = TOMLValueType.null() + alias STRING = TOMLValueType.string() + alias INTEGER = TOMLValueType.integer() + alias FLOAT = TOMLValueType.float() + alias BOOLEAN = TOMLValueType.boolean() + alias ARRAY = TOMLValueType.array() + alias TABLE = TOMLValueType.table() + + var value: Int + + # Static methods for each value type + @staticmethod + fn null() -> TOMLValueType: + return TOMLValueType(0) + + @staticmethod + fn string() -> TOMLValueType: + return TOMLValueType(1) + + @staticmethod + fn integer() -> TOMLValueType: + return TOMLValueType(2) + + @staticmethod + fn float() -> TOMLValueType: + return TOMLValueType(3) + + @staticmethod + fn boolean() -> TOMLValueType: + return TOMLValueType(4) + + @staticmethod + fn array() -> TOMLValueType: + return TOMLValueType(5) + + @staticmethod + fn table() -> TOMLValueType: + return TOMLValueType(6) + + # Constructor + fn __init__(out self, value: Int): + self.value = value + + # Equality comparison + fn __eq__(self, other: TOMLValueType) -> Bool: + return self.value == other.value + + fn __ne__(self, other: TOMLValueType) -> Bool: + return self.value != other.value + + # String representation for debugging + fn to_string(self) -> String: + if self == Self.NULL: + return "NULL" + elif self == Self.STRING: + return "STRING" + elif self == Self.INTEGER: + return "INTEGER" + elif self == Self.FLOAT: + return "FLOAT" + elif self == Self.BOOLEAN: + return "BOOLEAN" + elif self == Self.ARRAY: + return "ARRAY" + elif self == Self.TABLE: + return "TABLE" + else: + return "UNKNOWN" + + +@value +struct TOMLDocument: + """Represents a parsed TOML document.""" + + var root: Dict[String, TOMLValue] + + fn __init__(out self): + self.root = Dict[String, TOMLValue]() + + fn get(self, key: String) raises -> TOMLValue: + """Get a value from the document.""" + if key in self.root: + return self.root[key] + return TOMLValue() # Return empty/null value + + fn get_table(self, table_name: String) raises -> Dict[String, TOMLValue]: + """Get a table from the document.""" + if ( + table_name in self.root + and self.root[table_name].type == TOMLValueType.TABLE + ): + return self.root[table_name].table_values + return Dict[String, TOMLValue]() + + fn get_array(self, key: String) raises -> List[TOMLValue]: + """Get an array from the document.""" + if key in self.root and self.root[key].type == TOMLValueType.ARRAY: + return self.root[key].array_values + return List[TOMLValue]() + + fn get_array_of_tables( + self, key: String + ) raises -> List[Dict[String, TOMLValue]]: + """Get an array of tables from the document.""" + var result = List[Dict[String, TOMLValue]]() + + if key in self.root: + var value = self.root[key] + if value.type == TOMLValueType.ARRAY: + for table_value in value.array_values: + if table_value[].type == TOMLValueType.TABLE: + result.append(table_value[].table_values) + + return result + + +struct TOMLParser: + """Parses TOML source text into a TOMLDocument.""" + + var tokens: List[Token] + var current_index: Int + + fn __init__(out self, source: String): + var tokenizer = Tokenizer(source) + self.tokens = tokenizer.tokenize() + self.current_index = 0 + + fn __init__(out self, tokens: List[Token]): + self.tokens = tokens + self.current_index = 0 + + fn current_token(self) -> Token: + """Get the current token.""" + if self.current_index < len(self.tokens): + return self.tokens[self.current_index] + # Return EOF token if we're past the end + return Token(TokenType.EOF, "", 0, 0) + + fn advance(mut self): + """Move to the next token.""" + self.current_index += 1 + + fn parse_key_value(mut self) raises -> Tuple[String, TOMLValue]: + """Parse a key-value pair.""" + if self.current_token().type != TokenType.KEY: + return (String(""), TOMLValue()) + + var key = self.current_token().value + self.advance() + + if self.current_token().type != TokenType.EQUAL: + return (key, TOMLValue()) + self.advance() + + var value = self.parse_value() + return (key, value) + + fn parse_value(mut self) raises -> TOMLValue: + """Parse a TOML value.""" + var token = self.current_token() + self.advance() + + if token.type == TokenType.STRING: + return TOMLValue(token.value) + elif token.type == TokenType.INTEGER: + return TOMLValue(atol(token.value)) + elif token.type == TokenType.FLOAT: + return TOMLValue(atof(token.value)) + elif token.type == TokenType.KEY: + if token.value == "true": + return TOMLValue(True) + elif token.value == "false": + return TOMLValue(False) + # Default to string for other keys + return TOMLValue(token.value) + elif token.type == TokenType.ARRAY_START: + var array = List[TOMLValue]() + + # Parse values until we reach the end of the array + while ( + self.current_token().type != TokenType.ARRAY_END + and self.current_token().type != TokenType.EOF + ): + array.append(self.parse_value()) + + # Skip comma if present + if self.current_token().type == TokenType.COMMA: + self.advance() + + # Skip the closing bracket + if self.current_token().type == TokenType.ARRAY_END: + self.advance() + + var result = TOMLValue() + result.type = TOMLValueType.ARRAY + result.array_values = array + return result + + # Default to NULL value + return TOMLValue() + + fn parse_table(mut self) raises -> Tuple[String, Dict[String, TOMLValue]]: + """Parse a table section.""" + # Skip '[' token + self.advance() + + if self.current_token().type != TokenType.KEY: + return (String(""), Dict[String, TOMLValue]()) + + var table_name = self.current_token().value + self.advance() + + # Skip ']' token + if self.current_token().type == TokenType.ARRAY_END: + self.advance() + + var table_values = Dict[String, TOMLValue]() + + # Skip newline after table header + while self.current_token().type == TokenType.NEWLINE: + self.advance() + + var key: String + var value: TOMLValue + + # Parse key-value pairs until we reach a new table or EOF + while self.current_token().type == TokenType.KEY: + key, value = self.parse_key_value() + if key: + table_values[key] = value + + # Skip newline + if self.current_token().type == TokenType.NEWLINE: + self.advance() + + return (table_name, table_values) + + fn parse(mut self) raises -> TOMLDocument: + """Parse the tokens into a TOMLDocument.""" + var document = TOMLDocument() + + while self.current_index < len(self.tokens): + var token = self.current_token() + + if token.type == TokenType.NEWLINE: + self.advance() + continue + + elif token.type == TokenType.TABLE_START: + var table_name: String + var table_values: Dict[String, TOMLValue] + table_name, table_values = self.parse_table() + if table_name: + var table_value = TOMLValue() + table_value.type = TOMLValueType.TABLE + table_value.table_values = table_values + document.root[table_name] = table_value + + elif token.type == TokenType.ARRAY_OF_TABLES_START: + # Get table name + self.advance() + if self.current_token().type != TokenType.KEY: + self.advance() + continue + + var table_name = self.current_token().value + self.advance() + + # Skip closing brackets + if self.current_token().type == TokenType.ARRAY_END: + self.advance() + if self.current_token().type == TokenType.ARRAY_END: + self.advance() + + # Skip newlines + while self.current_token().type == TokenType.NEWLINE: + self.advance() + + # Parse table contents + var table_values = Dict[String, TOMLValue]() + + # Parse key-value pairs + while self.current_token().type == TokenType.KEY: + var key: String + var value: TOMLValue + key, value = self.parse_key_value() + if key: + table_values[key] = value + + # Skip newline + if self.current_token().type == TokenType.NEWLINE: + self.advance() + + # Create table value + var table_value = TOMLValue() + table_value.type = TOMLValueType.TABLE + table_value.table_values = table_values + + # Add to array of tables + if ( + table_name in document.root + and document.root[table_name].type == TOMLValueType.ARRAY + ): + # Array exists, append to it + document.root[table_name].array_values.append(table_value) + else: + # Create new array with this table + var array_value = TOMLValue() + array_value.type = TOMLValueType.ARRAY + array_value.array_values = List[TOMLValue]() + array_value.array_values.append(table_value) + document.root[table_name] = array_value + + elif token.type == TokenType.KEY: + var key: String + var value: TOMLValue + key, value = self.parse_key_value() + if key: + document.root[key] = value + + # Skip newline + if self.current_token().type == TokenType.NEWLINE: + self.advance() + + elif token.type == TokenType.ARRAY_START: + var table_name: String + var table_values: Dict[String, TOMLValue] + table_name, table_values = self.parse_table() + if table_name: + var table_value = TOMLValue() + table_value.type = TOMLValueType.TABLE + table_value.table_values = table_values + document.root[table_name] = table_value + else: + self.advance() + + return document + + +fn parse_string(input: String) raises -> TOMLDocument: + """Parse a TOML string into a document.""" + var parser = TOMLParser(input) + return parser.parse() + + +fn parse_file(file_path: String) raises -> TOMLDocument: + """Parse a TOML file into a document.""" + + with open(file_path, "r") as file: + content = file.read() + + return parse_string(content) diff --git a/src/tomlmojo/tokenizer.mojo b/src/tomlmojo/tokenizer.mojo new file mode 100644 index 00000000..b86d4542 --- /dev/null +++ b/src/tomlmojo/tokenizer.mojo @@ -0,0 +1,405 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +""" +A simple TOML tokenizer for Mojo. +This provides basic tokenization for TOML files, focusing on the core elements +needed for test case parsing. +""" + +alias WHITESPACE = " \t" +alias NEWLINE = "\n\r" +alias COMMENT_START = "#" +alias QUOTE = '"' +alias LITERAL_QUOTE = "'" + + +@value +struct Token: + """Represents a token in the TOML document.""" + + var type: TokenType + var value: String + var line: Int + var column: Int + + fn __init__( + out self, type: TokenType, value: String, line: Int, column: Int + ): + self.type = type + self.value = value + self.line = line + self.column = column + + +@value +struct SourcePosition: + """Tracks position in the source text.""" + + var line: Int + var column: Int + var index: Int + + fn __init__(out self, line: Int = 1, column: Int = 1, index: Int = 0): + self.line = line + self.column = column + self.index = index + + fn advance(mut self, char: String): + """Update position after consuming a character.""" + if char == "\n": + self.line += 1 + self.column = 1 + else: + self.column += 1 + self.index += 1 + + +@value +struct TokenType: + """ + TokenType mimics an enum for token types in TOML. + """ + + # Aliases for TokenType static methods to mimic enum constants + alias KEY = TokenType.key() + alias STRING = TokenType.string() + alias INTEGER = TokenType.integer() + alias FLOAT = TokenType.float() + alias BOOLEAN = TokenType.boolean() + alias DATETIME = TokenType.datetime() + alias ARRAY_START = TokenType.array_start() + alias ARRAY_END = TokenType.array_end() + alias TABLE_START = TokenType.table_start() + alias TABLE_END = TokenType.table_end() + alias ARRAY_OF_TABLES_START = TokenType.array_of_tables_start() + alias EQUAL = TokenType.equal() + alias COMMA = TokenType.comma() + alias NEWLINE = TokenType.newline() # Renamed to avoid conflict with NEWLINE constant + alias DOT = TokenType.dot() + alias EOF = TokenType.eof() + alias ERROR = TokenType.error() + + # Attributes + var value: Int + + # Token type constants (lowercase method names) + @staticmethod + fn key() -> TokenType: + return TokenType(0) + + @staticmethod + fn string() -> TokenType: + return TokenType(1) + + @staticmethod + fn integer() -> TokenType: + return TokenType(2) + + @staticmethod + fn float() -> TokenType: + return TokenType(3) + + @staticmethod + fn boolean() -> TokenType: + return TokenType(4) + + @staticmethod + fn datetime() -> TokenType: + return TokenType(5) + + @staticmethod + fn array_start() -> TokenType: + return TokenType(6) + + @staticmethod + fn array_end() -> TokenType: + return TokenType(7) + + @staticmethod + fn table_start() -> TokenType: + return TokenType(8) + + @staticmethod + fn table_end() -> TokenType: + return TokenType(9) + + @staticmethod + fn array_of_tables_start() -> TokenType: + return TokenType(16) + + @staticmethod + fn equal() -> TokenType: + return TokenType(10) + + @staticmethod + fn comma() -> TokenType: + return TokenType(11) + + @staticmethod + fn newline() -> TokenType: + return TokenType(12) + + @staticmethod + fn dot() -> TokenType: + return TokenType(13) + + @staticmethod + fn eof() -> TokenType: + return TokenType(14) + + @staticmethod + fn error() -> TokenType: + return TokenType(15) + + # Constructor + fn __init__(out self, value: Int): + self.value = value + + # Comparison operators + fn __eq__(self, other: TokenType) -> Bool: + return self.value == other.value + + fn __ne__(self, other: TokenType) -> Bool: + return self.value != other.value + + +struct Tokenizer: + """Tokenizes TOML source text.""" + + var source: String + var position: SourcePosition + var current_char: String + + fn __init__(out self, source: String): + self.source = source + self.position = SourcePosition() + if len(source) > 0: + self.current_char = String(source[0]) + else: + self.current_char = "" + + fn _get_char(self, index: Int) -> String: + """Get character at given index or empty string if out of bounds.""" + if index >= len(self.source): + return "" + return String(self.source[index]) + + fn _advance(mut self): + """Move to the next character.""" + self.position.advance(self.current_char) + self.current_char = self._get_char(self.position.index) + + fn _skip_whitespace(mut self): + """Skip whitespace characters.""" + while self.current_char and self.current_char in WHITESPACE: + self._advance() + + fn _skip_comment(mut self): + """Skip comment lines.""" + if self.current_char == COMMENT_START: + while self.current_char and self.current_char not in NEWLINE: + self._advance() + + fn _read_string(mut self) -> Token: + """Read a string value.""" + start_line = self.position.line + start_column = self.position.column + quote_char = self.current_char + result = String("") + + # Skip opening quote + self._advance() + + var chars = List[String]() + + while self.current_char and self.current_char != quote_char: + # Handle escape sequence + if ( + self.current_char == r"\\" + and self._get_char(self.position.index + 1) == quote_char + ): + self._advance() + chars.append(quote_char) + else: + chars.append(self.current_char) + self._advance() + + result = String.join("", chars) + + # Skip closing quote + if self.current_char == quote_char: + self._advance() + return Token(TokenType.STRING, result, start_line, start_column) + else: + return Token( + TokenType.ERROR, "Unterminated string", start_line, start_column + ) + + fn _read_number(mut self) -> Token: + """Read a number value.""" + start_line = self.position.line + start_column = self.position.column + result = String("") + is_float = False + + while self.current_char and ( + self.current_char.isdigit() or self.current_char == "." + ): + if self.current_char == ".": + is_float = True + result += self.current_char + self._advance() + + if is_float: + return Token(TokenType.FLOAT, result, start_line, start_column) + else: + return Token(TokenType.INTEGER, result, start_line, start_column) + + fn _read_key(mut self) -> Token: + """Read a key identifier.""" + start_line = self.position.line + start_column = self.position.column + result = String("") + + while self.current_char and ( + self.current_char.isdigit() + or self.current_char.isupper() + or self.current_char.islower() + or self.current_char == "_" + or self.current_char == "-" + ): + result += self.current_char + self._advance() + + return Token(TokenType.KEY, result, start_line, start_column) + + fn next_token(mut self) -> Token: + """Get the next token from the source.""" + self._skip_whitespace() + + if not self.current_char: + return Token( + TokenType.EOF, "", self.position.line, self.position.column + ) + + if self.current_char == COMMENT_START: + self._skip_comment() + return self.next_token() + + if self.current_char in NEWLINE: + token = Token( + TokenType.NEWLINE, + self.current_char, + self.position.line, + self.position.column, + ) + self._advance() + return token + + if self.current_char == "=": + token = Token( + TokenType.EQUAL, "=", self.position.line, self.position.column + ) + self._advance() + return token + + if self.current_char == ",": + token = Token( + TokenType.COMMA, ",", self.position.line, self.position.column + ) + self._advance() + return token + + if self.current_char == ".": + token = Token( + TokenType.DOT, ".", self.position.line, self.position.column + ) + self._advance() + return token + + if self.current_char == "[": + # Check if next char is also [ + if self._get_char(self.position.index + 1) == "[": + # This is an array of tables start + token = Token( + TokenType.ARRAY_OF_TABLES_START, + "[[", + self.position.line, + self.position.column, + ) + self._advance() # Skip first [ + self._advance() # Skip second [ + return token + else: + # Regular table start + token = Token( + TokenType.TABLE_START, + "[", + self.position.line, + self.position.column, + ) + self._advance() + return token + + if self.current_char == "]": + token = Token( + TokenType.ARRAY_END, + "]", + self.position.line, + self.position.column, + ) + self._advance() + return token + + if self.current_char == QUOTE or self.current_char == LITERAL_QUOTE: + return self._read_string() + + if self.current_char.isdigit(): + return self._read_number() + + if ( + self.current_char.isdigit() + or self.current_char.isupper() + or self.current_char.islower() + or self.current_char == "_" + ): + return self._read_key() + + # Unrecognized character + token = Token( + TokenType.ERROR, + "Unexpected character: " + self.current_char, + self.position.line, + self.position.column, + ) + self._advance() + return token + + fn tokenize(mut self) -> List[Token]: + """Tokenize the entire source text.""" + var tokens = List[Token]() + var token = self.next_token() + + while token.type != TokenType.EOF and token.type != TokenType.ERROR: + tokens.append(token) + token = self.next_token() + + # Add EOF token + if token.type == TokenType.EOF: + tokens.append(token) + + return tokens diff --git a/tests/biguint/test_biguint_arithmetics.mojo b/tests/biguint/test_biguint_arithmetics.mojo index 9125b485..10d332b8 100644 --- a/tests/biguint/test_biguint_arithmetics.mojo +++ b/tests/biguint/test_biguint_arithmetics.mojo @@ -4,72 +4,53 @@ BigUInt is an unsigned integer type, so it doesn't support negative values. """ from decimojo.biguint.biguint import BigUInt +from decimojo.tests import TestCase import testing +from tomlmojo import parse_file +alias file_path = "tests/biguint/test_data/biguint_arithmetics.toml" -fn test_add() raises: - print("------------------------------------------------------") - print("Testing BigUInt addition...") - # Test case 1: Simple addition of positive numbers - var a1 = BigUInt("123") - var b1 = BigUInt("456") - var result1 = a1 + b1 - testing.assert_equal( - String(result1), "579", "Simple addition of positive numbers" - ) +fn load_test_cases( + file_path: String, table_name: String +) raises -> List[TestCase]: + """Load test cases from a TOML file for a specific table.""" + var toml = parse_file(file_path) + var test_cases = List[TestCase]() - # Test case 2: Addition with zero - var a5 = BigUInt("123") - var b5 = BigUInt("0") - var result5 = a5 + b5 - testing.assert_equal(String(result5), "123", "Addition with zero") - - # Test case 3: Addition with large numbers - var a7 = BigUInt("99999999999999999999") - var b7 = BigUInt("1") - var result7 = a7 + b7 - testing.assert_equal( - String(result7), "100000000000000000000", "Addition with large numbers" - ) + # Get array of test cases + var cases_array = toml.get_array_of_tables(table_name) - # Test case 4: Addition causing multiple carries - var a8 = BigUInt("9999999999") - var b8 = BigUInt("1") - var result8 = a8 + b8 - testing.assert_equal( - String(result8), "10000000000", "Addition causing multiple carries" - ) + for i in range(len(cases_array)): + var case_table = cases_array[i] + test_cases.append( + TestCase( + case_table["a"].as_string(), + case_table["b"].as_string(), + case_table["expected"].as_string(), + case_table["description"].as_string(), + ) + ) - # Test case 5: Addition with numbers of different sizes - var a9 = BigUInt("12345") - var b9 = BigUInt("9876543210") - var result9 = a9 + b9 - testing.assert_equal( - String(result9), - "9876555555", - "Addition with numbers of different sizes", - ) + return test_cases - # Test case 6: Addition with very large numbers spanning multiple words - var a10 = BigUInt("12345678901234567890123456789") - var b10 = BigUInt("98765432109876543210987654321") - var result10 = a10 + b10 - testing.assert_equal( - String(result10), - "111111111011111111101111111110", - "Addition with very large numbers", - ) - # Test case 7: Adding numbers that require carry propagation through multiple words - var a11 = BigUInt("9" * 100) # A 100-digit number of all 9's - var b11 = BigUInt("1") - var result11 = a11 + b11 - testing.assert_equal( - String(result11), - "1" + "0" * 100, - "Addition with extensive carry propagation", - ) +fn test_add() raises: + print("------------------------------------------------------") + print("Testing BigUInt addition...") + + # Load test cases from TOML file + var test_cases = load_test_cases(file_path, "addition_tests") + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a + b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) print("BigUInt addition tests passed!") @@ -78,58 +59,30 @@ fn test_subtract() raises: print("------------------------------------------------------") print("Testing BigUInt subtraction...") - # Test case 1: Simple subtraction (larger - smaller) - var a1 = BigUInt("456") - var b1 = BigUInt("123") - var result1 = a1 - b1 - testing.assert_equal(String(result1), "333", "Simple subtraction") - - # Test case 2: Subtraction with zero - var a6 = BigUInt("123") - var b6 = BigUInt("0") - var result6 = a6 - b6 - testing.assert_equal(String(result6), "123", "Subtraction with zero") - - # Test case 3: Subtraction resulting in zero - var a8 = BigUInt("123") - var b8 = BigUInt("123") - var result8 = a8 - b8 - testing.assert_equal(String(result8), "0", "Subtraction resulting in zero") - - # Test case 4: Subtraction with borrow - var a9 = BigUInt("10000") - var b9 = BigUInt("1") - var result9 = a9 - b9 - testing.assert_equal(String(result9), "9999", "Subtraction with borrow") - - # Test case 5: Subtraction with multiple borrows - var a10 = BigUInt("10000") - var b10 = BigUInt("9999") - var result10 = a10 - b10 - testing.assert_equal( - String(result10), "1", "Subtraction with multiple borrows" - ) + # Load test cases from TOML file + var test_cases = load_test_cases(file_path, "subtraction_tests") - # Test case 6: Self subtraction (should be zero) - var a12 = BigUInt("12345678901234567890") - var result12 = a12 - a12 - testing.assert_equal( - String(result12), "0", "Self subtraction should yield zero" - ) + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a - b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) - # Test case 7: Test underflow handling - # Depending on implementation, this should either throw an error or wrap around - # Let's check if it throws an error + # Special case: Test underflow handling print("Testing underflow behavior (smaller - larger)...") - var a2 = BigUInt("123") - var b2 = BigUInt("456") - var exception_caught = False + var toml = parse_file(file_path) + var underflow_data = toml.get("subtraction_underflow") + var a_underflow = BigUInt(underflow_data.table_values["a"].as_string()) + var b_underflow = BigUInt(underflow_data.table_values["b"].as_string()) + try: - var result2 = a2 - b2 - print("Implementation allows underflow, result is: " + String(result2)) - # If no error, maybe BigUInt wraps around or has special underflow handling + var result = a_underflow - b_underflow + print("Implementation allows underflow, result is: " + String(result)) except: - exception_caught = True print("Implementation correctly throws error on underflow") print("BigUInt subtraction tests passed!") @@ -139,41 +92,21 @@ fn test_multiply() raises: print("------------------------------------------------------") print("Testing BigUInt multiplication...") - # Test case 1: Simple multiplication - var a1 = BigUInt("123") - var b1 = BigUInt("456") - var result1 = a1 * b1 - testing.assert_equal(String(result1), "56088", "Simple multiplication") - - # Test case 2: Multiplication by zero - var a2 = BigUInt("123456789") - var b2 = BigUInt("0") - var result2 = a2 * b2 - testing.assert_equal(String(result2), "0", "Multiplication by zero") - - # Test case 3: Multiplication by one - var a3 = BigUInt("123456789") - var b3 = BigUInt("1") - var result3 = a3 * b3 - testing.assert_equal(String(result3), "123456789", "Multiplication by one") - - # Test case 4: Multiplication of large numbers - var a4 = BigUInt("12345") - var b4 = BigUInt("67890") - var result4 = a4 * b4 - testing.assert_equal( - String(result4), "838102050", "Multiplication of large numbers" + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "multiplication_tests", ) - # Test case 5: Multiplication of very large numbers - var a5 = BigUInt("9" * 10) # 10 nines - var b5 = BigUInt("9" * 10) # 10 nines - var result5 = a5 * b5 - testing.assert_equal( - String(result5), - "9999999998" + "0" * 9 + "1", - "Multiplication of very large numbers", - ) + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a * b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) print("BigUInt multiplication tests passed!") @@ -182,39 +115,53 @@ fn test_extreme_cases() raises: print("------------------------------------------------------") print("Testing extreme cases...") - # Test case 1: Addition of very large numbers - var a1 = BigUInt("1" + "0" * 1000) # 10^1000 - var b1 = BigUInt("5" + "0" * 999) # 5×10^999 - var sum1 = a1 + b1 - testing.assert_equal( - String(sum1), "1" + "5" + "0" * 999, "Addition of very large numbers" + # Load extreme addition test cases from TOML file + var extreme_cases = load_test_cases( + file_path, + "extreme_addition_tests", ) - # Test case 2: Adding numbers that require carry propagation through many words - var a2 = BigUInt("9" * 100) # A 100-digit number of all 9's - var b2 = BigUInt("1") - var result2 = a2 + b2 - testing.assert_equal( - String(result2), - "1" + "0" * 100, - "Addition with extensive carry propagation", + # Run addition test cases + for i in range(len(extreme_cases)): + var test_case = extreme_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a + b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) + + # Load extreme subtraction test cases from TOML file + var subtraction_cases = load_test_cases( + file_path, + "extreme_subtraction_tests", ) - # Test case 3: Very large subtraction within range - var a3 = BigUInt("1" + "0" * 200) # 10^200 - var b3 = BigUInt("1") - var result3 = a3 - b3 - testing.assert_equal( - String(result3), "9" + "9" * 199, "Very large subtraction within range" + # Run subtraction test cases + for i in range(len(subtraction_cases)): + var test_case = subtraction_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a - b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) + + # Load extreme multiplication test cases from TOML file + var multiplication_cases = load_test_cases( + file_path, + "extreme_multiplication_tests", ) - # Test case 4: Very large multiplication - var a4 = BigUInt("1" + "0" * 10) # 10^10 - var b4 = BigUInt("1" + "0" * 10) # 10^10 - var result4 = a4 * b4 - testing.assert_equal( - String(result4), "1" + "0" * 20, "Very large multiplication" - ) + # Run multiplication test cases + for i in range(len(multiplication_cases)): + var test_case = multiplication_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a * b + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) print("Extreme case tests passed!") diff --git a/tests/biguint/test_biguint_truncate_divide.mojo b/tests/biguint/test_biguint_truncate_divide.mojo index 55be401d..68b31ed0 100644 --- a/tests/biguint/test_biguint_truncate_divide.mojo +++ b/tests/biguint/test_biguint_truncate_divide.mojo @@ -8,6 +8,34 @@ import testing from decimojo.biguint.biguint import BigUInt import decimojo.biguint.arithmetics from python import Python, PythonObject +from tomlmojo import parse_file +from decimojo.tests import TestCase + +alias file_path = "tests/biguint/test_data/biguint_truncate_divide.toml" + + +fn load_test_cases( + file_path: String, table_name: String +) raises -> List[TestCase]: + """Load test cases from a TOML file for a specific table.""" + var toml = parse_file(file_path) + var test_cases = List[TestCase]() + + # Get array of test cases + var cases_array = toml.get_array_of_tables(table_name) + + for i in range(len(cases_array)): + var case_table = cases_array[i] + test_cases.append( + TestCase( + case_table["a"].as_string(), + case_table["b"].as_string(), + "", # We don't need expected since we'll compare with Python + case_table["description"].as_string(), + ) + ) + + return test_cases fn test_basic_truncate_division() raises: @@ -17,59 +45,31 @@ fn test_basic_truncate_division() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test case 1: Division with no remainder - var a1 = BigUInt("10") - var b1 = BigUInt("2") - var result1 = a1 // b1 - var py_result1 = py.int("10") // py.int("2") - testing.assert_equal( - String(result1), "5", "10 / 2 should equal 5, got " + String(result1) - ) - testing.assert_equal( - String(result1), - String(py_result1), - "Result doesn't match Python's int result", + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "basic_division_tests", ) - # Test case 2: Division with remainder (truncate toward zero) - var a2 = BigUInt("10") - var b2 = BigUInt("3") - var result2 = a2 // b2 - testing.assert_equal( - String(result2), "3", "10 / 3 should equal 3, got " + String(result2) - ) + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a // b - # Test case 3: Division results in zero (smaller / larger) - var a3 = BigUInt("3") - var b3 = BigUInt("10") - var result3 = a3 // b3 - testing.assert_equal( - String(result3), "0", "3 / 10 should equal 0, got " + String(result3) - ) + # Compare with Python's result + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_result = py_a // py_b - # Test case 4: Division by 1 - var a4 = BigUInt("42") - var b4 = BigUInt("1") - var result4 = a4 // b4 - testing.assert_equal( - String(result4), "42", "42 / 1 should equal 42, got " + String(result4) - ) - - # Test case 5: Large number division - var a5 = BigUInt("1000000000000") - var b5 = BigUInt("1000000") - var result5 = a5 // b5 - var py_result5 = py.int("1000000000000") // py.int("1000000") - testing.assert_equal( - String(result5), - "1000000", - "1000000000000 / 1000000 should equal 1000000, got " + String(result5), - ) - testing.assert_equal( - String(result5), - String(py_result5), - "Result doesn't match Python's int result", - ) + testing.assert_equal( + String(result), + String(py_result), + test_case.description + + " - Result doesn't match Python's int result", + ) + print("✓ " + test_case.description) print("✓ Basic truncate division tests passed!") @@ -81,31 +81,49 @@ fn test_zero_handling() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test case 1: Zero dividend - var a1 = BigUInt("0") - var b1 = BigUInt("5") - var result1 = a1 // b1 - var py_result1 = py.int("0") // py.int("5") - testing.assert_equal( - String(result1), "0", "0 / 5 should equal 0, got " + String(result1) - ) - testing.assert_equal( - String(result1), - String(py_result1), - "Result doesn't match Python's int result", - ) + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "zero_handling_tests", + ) + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a // b + + # Compare with Python's result + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_result = py_a // py_b + + testing.assert_equal( + String(result), + String(py_result), + test_case.description + + " - Result doesn't match Python's int result", + ) + print("✓ " + test_case.description) + + # Special test for division by zero + print("Testing division by zero error handling...") + var toml = parse_file(file_path) + var div_zero_data = toml.get("division_by_zero") + var a_zero = BigUInt(div_zero_data.table_values["a"].as_string()) + var b_zero = BigUInt(div_zero_data.table_values["b"].as_string()) - # Test case 2: Division by zero should raise an error - var a2 = BigUInt("10") - var b2 = BigUInt("0") var exception_caught = False try: - var _result2 = a2 // b2 + var _result = a_zero // b_zero except: exception_caught = True + testing.assert_true( exception_caught, "Division by zero should raise an error" ) + print("✓ Division by zero correctly raises an error") print("✓ Zero handling tests passed!") @@ -117,113 +135,31 @@ fn test_large_number_division() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test case 1: Large number divided by small number - var a1 = BigUInt("1" + "0" * 50) # 10^50 - var b1 = BigUInt("7") - var expected1 = BigUInt( - "14285714285714285714285714285714285714285714285714" - ) # 10^50 / 7 = 14285714285714285714285714... - var result1 = a1 // b1 - var py_result1 = py.int("1" + "0" * 50) // py.int("7") - testing.assert_equal( - String(result1), - String(expected1), - "Large number division gave incorrect result", - ) - testing.assert_equal( - String(result1), - String(py_result1), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a1, b1, result1)) - - # Test case 2: Large number divided by large number - var a2 = BigUInt("9" * 30) # 30 nines - var b2 = BigUInt("9" * 15) # 15 nines - var expected2 = BigUInt("1" + "0" * 14 + "1") # 10^15 + 1 - var result2 = a2 // b2 - var py_result2 = py.int("9" * 30) // py.int("9" * 15) - testing.assert_equal( - String(result2), - String(expected2), - "Large / large division gave incorrect result", - ) - testing.assert_equal( - String(result2), - String(py_result2), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a2, b2, result2)) - - # Test case 3: Very large number divisible by power of 10 - var a3 = BigUInt("1" + "0" * 100) # 10^100 - var b3 = BigUInt("1" + "0" * 40) # 10^40 - var expected3 = BigUInt("1" + "0" * 60) # 10^60 - var result3 = a3 // b3 - var py_result3 = py.int("1" + "0" * 100) // py.int("1" + "0" * 40) - testing.assert_equal( - String(result3), - String(expected3), - "Power of 10 division gave incorrect result", - ) - testing.assert_equal( - String(result3), - String(py_result3), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a3, b3, result3)) - - # Test case 4: Large number with large divisor resulting in small quotient - var a4 = BigUInt("9" * 50) # 50 nines - var b4 = BigUInt("3" * 49 + "4") # slightly less than a third of a4 - var result4 = a4 // b4 - var py_result4 = py.int("9" * 50) // py.int("3" * 49 + "4") - testing.assert_equal( - String(result4), - "2", - ( - "Large numbers division resulting in small quotient gave incorrect" - " result" - ), + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "large_number_tests", ) - testing.assert_equal( - String(result4), - String(py_result4), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a4, b4, result4)) - - # Test case 5: Large number with very large divisor - # x1 is more than twice the length of x2 - # x2 is 20 words long (>= 10^180) - stra = "123456789" * 50 - strb = "987654321" * 20 - var a5 = BigUInt(stra) - var b5 = BigUInt(strb) - var result5 = a5 // b5 - var py_result5 = py.int(stra) // py.int(strb) - testing.assert_equal( - String(result5), - String(py_result5), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a5, b5, result5)) - - # Test case 5: Large number with very large divisor - # x1 is more than 200 words long (>= 10^1800) - # x2 is more than 50 words long (>= 10^450) - stra = "123456789" * 250 - strb = "987654321" * 100 - var a6 = BigUInt(stra) - var b6 = BigUInt(strb) - var result6 = a6 // b6 - var py_result6 = py.int(stra) // py.int(strb) - testing.assert_equal( - String(result6), - String(py_result6), - "Result doesn't match Python's int result", - ) - print("passed: {} / {} = {}".format(a6, b6, result6)) + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a // b + + # Compare with Python's result + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_result = py_a // py_b + + testing.assert_equal( + String(result), + String(py_result), + test_case.description + + " - Result doesn't match Python's int result", + ) + print("✓ " + test_case.description) print("✓ Large number division tests passed!") @@ -235,73 +171,31 @@ fn test_division_rounding() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test case 1: 7/2 = 3.5 -> 3 - var a1 = BigUInt("7") - var b1 = BigUInt("2") - var expected1 = BigUInt("3") - var result1 = a1 // b1 - var py_result1 = py.int("7") // py.int("2") - testing.assert_equal( - String(result1), - String(expected1), - "7 / 2 should equal 3, got " + String(result1), - ) - testing.assert_equal( - String(result1), - String(py_result1), - "Result doesn't match Python's int result", + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "division_rounding_tests", ) - # Test case 2: 1/3 = 0.333... -> 0 - var a2 = BigUInt("1") - var b2 = BigUInt("3") - var expected2 = BigUInt("0") - var result2 = a2 // b2 - var py_result2 = py.int("1") // py.int("3") - testing.assert_equal( - String(result2), - String(expected2), - "1 / 3 should equal 0, got " + String(result2), - ) - testing.assert_equal( - String(result2), - String(py_result2), - "Result doesn't match Python's int result", - ) + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a // b - # Test case 3: 5/4 = 1.25 -> 1 - var a3 = BigUInt("5") - var b3 = BigUInt("4") - var expected3 = BigUInt("1") - var result3 = a3 // b3 - var py_result3 = py.int("5") // py.int("4") - testing.assert_equal( - String(result3), - String(expected3), - "5 / 4 should equal 1, got " + String(result3), - ) - testing.assert_equal( - String(result3), - String(py_result3), - "Result doesn't match Python's int result", - ) + # Compare with Python's result + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_result = py_a // py_b - # Test case 4: 99/100 = 0.99 -> 0 - var a4 = BigUInt("99") - var b4 = BigUInt("100") - var expected4 = BigUInt("0") - var result4 = a4 // b4 - var py_result4 = py.int("99") // py.int("100") - testing.assert_equal( - String(result4), - String(expected4), - "99 / 100 should equal 0, got " + String(result4), - ) - testing.assert_equal( - String(result4), - String(py_result4), - "Result doesn't match Python's int result", - ) + testing.assert_equal( + String(result), + String(py_result), + test_case.description + + " - Result doesn't match Python's int result", + ) + print("✓ " + test_case.description) print("✓ Division rounding tests passed!") @@ -313,72 +207,58 @@ fn test_division_identity() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test property: (a / b) * b + (a % b) = a - var a1 = BigUInt("17") - var b1 = BigUInt("5") - var quotient1 = a1 // b1 # 3 - var remainder1 = a1 % b1 # 2 - var reconstructed1 = quotient1 * b1 + remainder1 # 3*5 + 2 = 17 - - # Python equivalent - var py_a1 = py.int("17") - var py_b1 = py.int("5") - var py_quotient1 = py_a1 // py_b1 - var py_remainder1 = py_a1 % py_b1 - var py_reconstructed1 = py_quotient1 * py_b1 + py_remainder1 - - testing.assert_equal( - String(reconstructed1), - String(a1), - "(a / b) * b + (a % b) should equal a for positive numbers", - ) - testing.assert_equal( - String(quotient1), - String(py_quotient1), - "Quotient doesn't match Python's int result", - ) - testing.assert_equal( - String(remainder1), - String(py_remainder1), - "Remainder doesn't match Python's int result", - ) - testing.assert_equal( - String(reconstructed1), - String(py_reconstructed1), - "Reconstructed value doesn't match Python's result", - ) - - # Test case with larger numbers - var a2 = BigUInt("12345678901234567890") - var b2 = BigUInt("987654321") - var quotient2 = a2 // b2 - var remainder2 = a2 % b2 - var reconstructed2 = quotient2 * b2 + remainder2 - var py_a2 = py.int("12345678901234567890") - var py_b2 = py.int("987654321") - var py_quotient2 = py_a2 // py_b2 - var py_remainder2 = py_a2 % py_b2 - var py_reconstructed2 = py_quotient2 * py_b2 + py_remainder2 - testing.assert_equal( - String(reconstructed2), - String(a2), - "(a / b) * b + (a % b) should equal a for large numbers", - ) - testing.assert_equal( - String(quotient2), - String(py_quotient2), - "Quotient doesn't match Python's int result", - ) - testing.assert_equal( - String(remainder2), - String(py_remainder2), - "Remainder doesn't match Python's int result", - ) - testing.assert_equal( - String(reconstructed2), - String(py_reconstructed2), - "Reconstructed value doesn't match Python's result", - ) + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "division_identity_tests", + ) + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + + # Mojo calculations + var quotient = a // b + var remainder = a % b + var reconstructed = quotient * b + remainder + + # Python calculations + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_quotient = py_a // py_b + var py_remainder = py_a % py_b + var py_reconstructed = py_quotient * py_b + py_remainder + + # Verify all parts match Python and the identity holds + testing.assert_equal( + String(quotient), + String(py_quotient), + test_case.description + " - Quotient doesn't match Python's result", + ) + + testing.assert_equal( + String(remainder), + String(py_remainder), + test_case.description + + " - Remainder doesn't match Python's result", + ) + + testing.assert_equal( + String(reconstructed), + String(a), + test_case.description + " - (a / b) * b + (a % b) should equal a", + ) + + testing.assert_equal( + String(reconstructed), + String(py_reconstructed), + test_case.description + + " - Reconstructed value doesn't match Python's result", + ) + + print("✓ " + test_case.description) print("✓ Mathematical identity tests passed!") @@ -390,86 +270,31 @@ fn test_edge_cases() raises: # Get Python's built-in int module var py = Python.import_module("builtins") - # Test case 1: Maximum divisor - # Dividing by a number almost as large as the dividend - var a1 = BigUInt("1000") - var b1 = BigUInt("999") - var result1 = a1 // b1 - var py_result1 = py.int("1000") // py.int("999") - testing.assert_equal( - String(result1), - "1", - "1000 / 999 should equal 1, got " + String(result1), - ) - testing.assert_equal( - String(result1), - String(py_result1), - "Result doesn't match Python's int result", - ) - - # Test case 2: Consecutive numbers - var a2 = BigUInt("101") - var b2 = BigUInt("100") - var result2 = a2 // b2 - var py_result2 = py.int("101") // py.int("100") - testing.assert_equal( - String(result2), - "1", - "101 / 100 should equal 1, got " + String(result2), - ) - testing.assert_equal( - String(result2), - String(py_result2), - "Result doesn't match Python's int result", - ) - - # Test case 3: Equal large numbers - var a3 = BigUInt("9" * 100) - var b3 = BigUInt("9" * 100) - var result3 = a3 // b3 - var py_result3 = py.int("9" * 100) // py.int("9" * 100) - testing.assert_equal( - String(result3), - "1", - "Equal large numbers division should equal 1", - ) - testing.assert_equal( - String(result3), - String(py_result3), - "Result doesn't match Python's int result", - ) - - # Test case 4: Powers of 10 - var a4 = BigUInt("1" + "0" * 20) # 10^20 - var b4 = BigUInt("1" + "0" * 10) # 10^10 - var result4 = a4 // b4 - var py_result4 = py.int("1" + "0" * 20) // py.int("1" + "0" * 10) - testing.assert_equal( - String(result4), - "1" + "0" * 10, # 10^10 - "Powers of 10 division gave incorrect result", - ) - testing.assert_equal( - String(result4), - String(py_result4), - "Result doesn't match Python's int result", - ) - - # Test case 5: Division resulting in large quotient - var a5 = BigUInt("2" + "0" * 200) # 2 × 10^200 - var b5 = BigUInt("2") - var result5 = a5 // b5 - var py_result5 = py.int("2" + "0" * 200) // py.int("2") - testing.assert_equal( - String(result5), - "1" + "0" * 200, # 10^200 - "Large quotient division gave incorrect result", - ) - testing.assert_equal( - String(result5), - String(py_result5), - "Result doesn't match Python's int result", - ) + # Load test cases from TOML file + var test_cases = load_test_cases( + file_path, + "edge_case_tests", + ) + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigUInt(test_case.a) + var b = BigUInt(test_case.b) + var result = a // b + + # Compare with Python's result + var py_a = py.int(test_case.a) + var py_b = py.int(test_case.b) + var py_result = py_a // py_b + + testing.assert_equal( + String(result), + String(py_result), + test_case.description + + " - Result doesn't match Python's int result", + ) + print("✓ " + test_case.description) print("✓ Edge cases tests passed!") diff --git a/tests/biguint/test_data/biguint_arithmetics.toml b/tests/biguint/test_data/biguint_arithmetics.toml new file mode 100644 index 00000000..20006278 --- /dev/null +++ b/tests/biguint/test_data/biguint_arithmetics.toml @@ -0,0 +1,160 @@ +# ===----------------------------------------------------------------------=== # +# Test data for BigUInt arithmetic operations +# - Test cases for BigUInt addition +# - Test cases for BigUInt subtraction +# - Test cases for BigUInt multiplication +# - Special test for subtraction underflow +# - Extreme test cases +# ===----------------------------------------------------------------------=== # + +# ===----------------------------------------------------------------------=== # +# Test cases for BigUInt addition +# ===----------------------------------------------------------------------=== # +[[addition_tests]] +a = "123" +b = "456" +expected = "579" +description = "Simple addition of positive numbers" + +[[addition_tests]] +a = "123" +b = "0" +expected = "123" +description = "Addition with zero" + +[[addition_tests]] +a = "99999999999999999999" +b = "1" +expected = "100000000000000000000" +description = "Addition with large numbers" + +[[addition_tests]] +a = "9999999999" +b = "1" +expected = "10000000000" +description = "Addition causing multiple carries" + +[[addition_tests]] +a = "12345" +b = "9876543210" +expected = "9876555555" +description = "Addition with numbers of different sizes" + +[[addition_tests]] +a = "12345678901234567890123456789" +b = "98765432109876543210987654321" +expected = "111111111011111111101111111110" +description = "Addition with very large numbers" + +[[addition_tests]] +a = "9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +b = "1" +expected = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +description = "Addition with extensive carry propagation" + +# ===----------------------------------------------------------------------=== # +# Test cases for BigUInt subtraction +# ===----------------------------------------------------------------------=== # +[[subtraction_tests]] +a = "456" +b = "123" +expected = "333" +description = "Simple subtraction" + +[[subtraction_tests]] +a = "123" +b = "0" +expected = "123" +description = "Subtraction with zero" + +[[subtraction_tests]] +a = "123" +b = "123" +expected = "0" +description = "Subtraction resulting in zero" + +[[subtraction_tests]] +a = "10000" +b = "1" +expected = "9999" +description = "Subtraction with borrow" + +[[subtraction_tests]] +a = "10000" +b = "9999" +expected = "1" +description = "Subtraction with multiple borrows" + +[[subtraction_tests]] +a = "12345678901234567890" +b = "12345678901234567890" +expected = "0" +description = "Self subtraction should yield zero" + +# ===----------------------------------------------------------------------=== # +# Special test for subtraction underflow +# ===----------------------------------------------------------------------=== # +[subtraction_underflow] +a = "123" +b = "456" +description = "Underflow handling (smaller - larger)" + +# ===----------------------------------------------------------------------=== # +# Test cases for BigUInt multiplication +# ===----------------------------------------------------------------------=== # +[[multiplication_tests]] +a = "123" +b = "456" +expected = "56088" +description = "Simple multiplication" + +[[multiplication_tests]] +a = "123456789" +b = "0" +expected = "0" +description = "Multiplication by zero" + +[[multiplication_tests]] +a = "123456789" +b = "1" +expected = "123456789" +description = "Multiplication by one" + +[[multiplication_tests]] +a = "12345" +b = "67890" +expected = "838102050" +description = "Multiplication of large numbers" + +[[multiplication_tests]] +a = "9999999999" +b = "9999999999" +expected = "99999999980000000001" +description = "Multiplication of very large numbers" + +# ===----------------------------------------------------------------------=== # +# Extreme test cases +# ===----------------------------------------------------------------------=== # +[[extreme_addition_tests]] +a = "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +b = "5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +expected = "1005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +description = "Addition of very large numbers" + +[[extreme_addition_tests]] +a = "9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +b = "1" +expected = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +description = "Addition with extensive carry propagation" + +[[extreme_subtraction_tests]] +a = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +b = "1" +expected = "9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +description = "Very large subtraction within range" + +[[extreme_multiplication_tests]] +a = "10000000000" +b = "10000000000" +expected = "100000000000000000000" +description = "Very large multiplication" \ No newline at end of file diff --git a/tests/biguint/test_data/biguint_truncate_divide.toml b/tests/biguint/test_data/biguint_truncate_divide.toml new file mode 100644 index 00000000..2ddb9be6 --- /dev/null +++ b/tests/biguint/test_data/biguint_truncate_divide.toml @@ -0,0 +1,144 @@ +# ===----------------------------------------------------------------------=== # +# Test data for BigUInt arithmetic operations +# - Basic division test cases +# - Zero handling tests +# - Large number division tests +# - Division rounding tests +# - Division identity tests +# - Edge cases +# - Division by zero test (special case - not in a loop) +# ===----------------------------------------------------------------------=== # + +# ===----------------------------------------------------------------------=== # +# Basic division test cases +# ===----------------------------------------------------------------------=== # +[[basic_division_tests]] +a = "10" +b = "2" +description = "Division with no remainder" + +[[basic_division_tests]] +a = "10" +b = "3" +description = "Division with remainder (truncate toward zero)" + +[[basic_division_tests]] +a = "3" +b = "10" +description = "Division results in zero (smaller / larger)" + +[[basic_division_tests]] +a = "42" +b = "1" +description = "Division by 1" + +[[basic_division_tests]] +a = "1000000000000" +b = "1000000" +description = "Large number division" + +# ===----------------------------------------------------------------------=== # +# Zero handling tests +# ===----------------------------------------------------------------------=== # +[[zero_handling_tests]] +a = "0" +b = "5" +description = "Zero dividend" + +# ===----------------------------------------------------------------------=== # +# Large number division tests +# ===----------------------------------------------------------------------=== # +[[large_number_tests]] +a = "10000000000000000000000000000000000000000000000000000" +b = "7" +description = "Large number divided by small number" + +[[large_number_tests]] +a = "999999999999999999999999999999" +b = "999999999999999" +description = "Large number divided by large number" + +[[large_number_tests]] +a = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +b = "10000000000000000000000000000000000000000000" +description = "Very large number divisible by power of 10" + +[[large_number_tests]] +a = "999999999999999999999999999999999999999999999999999999" +b = "334999999999999999999999999999999999999994" +description = "Large number with large divisor resulting in small quotient" + +[[large_number_tests]] +a = "123456789123456789123456789123456789123456789123456789123456789123456789123456789" +b = "987654321987654321987654321987654321" +description = "Large number with very large divisor" + +# ===----------------------------------------------------------------------=== # +# Division rounding tests +# ===----------------------------------------------------------------------=== # +[[division_rounding_tests]] +a = "7" +b = "2" +description = "7/2 = 3.5 -> 3" + +[[division_rounding_tests]] +a = "1" +b = "3" +description = "1/3 = 0.333... -> 0" + +[[division_rounding_tests]] +a = "5" +b = "4" +description = "5/4 = 1.25 -> 1" + +[[division_rounding_tests]] +a = "99" +b = "100" +description = "99/100 = 0.99 -> 0" + +# ===----------------------------------------------------------------------=== # +# Division identity tests +# ===----------------------------------------------------------------------=== # +[[division_identity_tests]] +a = "17" +b = "5" +description = "Testing (a / b) * b + (a % b) = a for positive numbers" + +[[division_identity_tests]] +a = "12345678901234567890" +b = "987654321" +description = "Testing (a / b) * b + (a % b) = a for large numbers" + +# Edge cases +[[edge_case_tests]] +a = "1000" +b = "999" +description = "Maximum divisor" + +[[edge_case_tests]] +a = "101" +b = "100" +description = "Consecutive numbers" + +[[edge_case_tests]] +a = "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +b = "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +description = "Equal large numbers" + +[[edge_case_tests]] +a = "100000000000000000000" +b = "10000000000" +description = "Powers of 10" + +[[edge_case_tests]] +a = "20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +b = "2" +description = "Division resulting in large quotient" + +# ===----------------------------------------------------------------------=== # +# Division by zero test (special case - not in a loop) +# ===----------------------------------------------------------------------=== # +[division_by_zero] +a = "10" +b = "0" +description = "Division by zero should raise an error" \ No newline at end of file