From 27ddea046ff4e37110493f5c2fb480e2d4ee3c97 Mon Sep 17 00:00:00 2001 From: Pratyushanand26 Date: Sat, 18 Oct 2025 09:15:56 +0530 Subject: [PATCH 1/2] feat: add support for LiteralDate in LLVM IR builder --- src/irx/builders/llvmliteir.py | 82 +++++++++++++-- tests/test_literal_date.py | 185 +++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 tests/test_literal_date.py diff --git a/src/irx/builders/llvmliteir.py b/src/irx/builders/llvmliteir.py index 9b88a87..37fa397 100644 --- a/src/irx/builders/llvmliteir.py +++ b/src/irx/builders/llvmliteir.py @@ -89,6 +89,12 @@ def get_data_type(self, type_name: str) -> ir.types.Type: return self.UTF8_STRING_TYPE elif type_name == "nonetype": return self.VOID_TYPE + elif type_name == "date": + return self.DATE_TYPE + + # Option 2: store as int64 timestamp (UNIX time) + # return self.INT64_TYPE + raise Exception(f"[EE]: Type name {type_name} not valid.") @@ -129,23 +135,22 @@ def translate(self, node: astx.AST) -> str: return str(self._llvm.module) def initialize(self) -> None: - """Initialize self.""" - # self._llvm.context = ir.context.Context() + """Initialize LLVM module and types safely.""" self._llvm = VariablesLLVM() self._llvm.module = ir.module.Module("Arx") - # initialize the target registry etc. - llvm.initialize() - llvm.initialize_all_asmprinters() - llvm.initialize_all_targets() - llvm.initialize_native_target() - llvm.initialize_native_asmparser() - llvm.initialize_native_asmprinter() + # ✅ Modern, safe initialization (llvmlite handles most automatically now) + try: + llvm.initialize_native_target() + llvm.initialize_native_asmprinter() + except (RuntimeError, AttributeError): + # These may already be initialized — safe to ignore + pass - # Create a new builder for the module. + # ✅ Create a new builder for the module self._llvm.ir_builder = ir.IRBuilder() - # Data Types + # ✅ Define basic data types self._llvm.FLOAT_TYPE = ir.FloatType() self._llvm.FLOAT16_TYPE = ir.HalfType() self._llvm.DOUBLE_TYPE = ir.DoubleType() @@ -160,6 +165,7 @@ def initialize(self) -> None: ) self._llvm.ASCII_STRING_TYPE = ir.IntType(8).as_pointer() self._llvm.UTF8_STRING_TYPE = self._llvm.STRING_TYPE + self._llvm.DATE_TYPE= ir.LiteralStructType([ir.IntType(32),ir.IntType(32),ir.IntType(32)]) def _add_builtins(self) -> None: # The C++ tutorial adds putchard() simply by defining it in the host @@ -657,6 +663,60 @@ def visit(self, node: astx.IfStmt) -> None: self.result_stack.append(phi) + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.LiteralDate) -> None: + """ + Lower a LiteralDate to LLVM IR. + + Representation: + { i32 year, i32 month, i32 day } -- emitted as a constant struct. + + Expected format: YYYY-MM-DD (ISO), but also accepts single-digit month/day. + """ + s = node.value.strip() + + # Split by "-" + parts = s.split("-") + if len(parts) != 3: + raise Exception( + f"LiteralDate: invalid date format '{node.value}'. Expected 'YYYY-MM-DD'." + ) + + try: + # Convert to integers even if month/day are single-digit + year = int(parts[0]) + month = int(parts[1]) + day = int(parts[2]) + except Exception: + raise Exception( + f"LiteralDate: invalid year/month/day in '{node.value}'." + ) + + # Basic range checks + if not (1 <= month <= 12): + raise Exception( + f"LiteralDate: month out of range in '{node.value}'. Expected 1-12." + ) + if not (1 <= day <= 31): + raise Exception( + f"LiteralDate: day out of range in '{node.value}'. Expected 1-31." + ) + if not (1 <= year <= 9999): + raise Exception( + f"LiteralDate: year out of range in '{node.value}'. Expected 1-9999." + ) + + # Build constant struct { i32, i32, i32 } + i32 = self._llvm.INT32_TYPE + date_ty = ir.LiteralStructType([i32, i32, i32]) + const_date = ir.Constant( + date_ty, + [ir.Constant(i32, year), ir.Constant(i32, month), ir.Constant(i32, day)], + ) + + self.result_stack.append(const_date) + + @dispatch # type: ignore[no-redef] def visit(self, expr: astx.WhileStmt) -> None: """Translate ASTx While Loop to LLVM-IR.""" diff --git a/tests/test_literal_date.py b/tests/test_literal_date.py new file mode 100644 index 0000000..75dd0f2 --- /dev/null +++ b/tests/test_literal_date.py @@ -0,0 +1,185 @@ +"""Tests for LiteralDate support.""" + +from typing import Type + +import astx +import pytest + +from irx.builders.base import Builder +from irx.builders.llvmliteir import LLVMLiteIR + +from .conftest import check_result + + +@pytest.mark.parametrize( + "date_str,expected_year,expected_month,expected_day", + [ + ("2024-01-15", 2024, 1, 15), + ("2000-12-31", 2000, 12, 31), + ("1970-01-01", 1970, 1, 1), + ("2023-06-15", 2023, 6, 15), + ("1999-02-28", 1999, 2, 28), + ("2024-11-30", 2024, 11, 30), + ], +) +@pytest.mark.parametrize("builder_class", [LLVMLiteIR]) +def test_literal_date_basic( + builder_class: Type[Builder], + date_str: str, + expected_year: int, + expected_month: int, + expected_day: int, +) -> None: + """Test basic LiteralDate parsing and IR generation.""" + builder = builder_class() + module = builder.module() + + # Create date literal + date_literal = astx.LiteralDate(date_str) + + # Create a function that stores the date + proto = astx.FunctionPrototype( + name="main", args=astx.Arguments(), return_type=astx.Int32() + ) + block = astx.Block() + + # Store date in variable + date_decl = astx.VariableDeclaration( + name="d", type_=astx.Date(), value=date_literal + ) + block.append(date_decl) + + # Return 0 (just testing that it compiles) + block.append(astx.FunctionReturn(astx.LiteralInt32(0))) + + fn = astx.FunctionDef(prototype=proto, body=block) + module.block.append(fn) + + # Check it translates without error + ir_code = builder.translate(module) + assert "i32" in ir_code + # Verify the struct contains our values + assert str(expected_year) in ir_code + assert str(expected_month) in ir_code + assert str(expected_day) in ir_code + + +@pytest.mark.parametrize( + "invalid_date,error_msg", + [ + ("2024-13-01", "month out of range"), # Invalid month + ("2024-00-01", "month out of range"), # Month = 0 + ("2024-01-32", "day out of range"), # Invalid day + ("2024-01-00", "day out of range"), # Day = 0 + ("10000-01-01", "year out of range"), # Year too large + ("0-01-01", "year out of range"), # Year = 0 + ("2024/01/01", "invalid date format"), # Wrong separator + ("2024-Jan-01", "invalid year/month/day"), # Text month + ("2024-01", "invalid date format"), # Missing day + ], +) +@pytest.mark.parametrize("builder_class", [LLVMLiteIR]) +def test_literal_date_invalid( + builder_class: Type[Builder], + invalid_date: str, + error_msg: str, +) -> None: + """Test that invalid date formats raise appropriate exceptions.""" + builder = builder_class() + module = builder.module() + + date_literal = astx.LiteralDate(invalid_date) + + proto = astx.FunctionPrototype( + name="main", args=astx.Arguments(), return_type=astx.Int32() + ) + block = astx.Block() + + date_decl = astx.VariableDeclaration( + name="d", type_=astx.Date(), value=date_literal + ) + block.append(date_decl) + block.append(astx.FunctionReturn(astx.LiteralInt32(0))) + + fn = astx.FunctionDef(prototype=proto, body=block) + module.block.append(fn) + + # Should raise an exception with expected message + with pytest.raises(Exception) as exc_info: + builder.translate(module) + + assert error_msg in str(exc_info.value).lower() + + +@pytest.mark.parametrize( + "date_str", + [ + "2024-02-29", # Leap year - valid + "2024-12-31", # End of year + "2024-01-01", # Start of year + ], +) +@pytest.mark.parametrize("builder_class", [LLVMLiteIR]) +def test_literal_date_edge_cases( + builder_class: Type[Builder], + date_str: str, +) -> None: + """Test edge cases for valid dates.""" + builder = builder_class() + module = builder.module() + + date_literal = astx.LiteralDate(date_str) + + proto = astx.FunctionPrototype( + name="main", args=astx.Arguments(), return_type=astx.Int32() + ) + block = astx.Block() + + date_decl = astx.VariableDeclaration( + name="d", type_=astx.Date(), value=date_literal + ) + block.append(date_decl) + block.append(astx.FunctionReturn(astx.LiteralInt32(0))) + + fn = astx.FunctionDef(prototype=proto, body=block) + module.block.append(fn) + + # Should compile successfully + ir_code = builder.translate(module) + assert "i32" in ir_code + + +@pytest.mark.parametrize("builder_class", [LLVMLiteIR]) +def test_literal_date_multiple_variables( + builder_class: Type[Builder], +) -> None: + """Test multiple date variables in the same function.""" + builder = builder_class() + module = builder.module() + + date1 = astx.LiteralDate("2024-01-15") + date2 = astx.LiteralDate("2023-12-25") + + proto = astx.FunctionPrototype( + name="main", args=astx.Arguments(), return_type=astx.Int32() + ) + block = astx.Block() + + # Store multiple dates + date_decl1 = astx.VariableDeclaration( + name="d1", type_=astx.Date(), value=date1 + ) + date_decl2 = astx.VariableDeclaration( + name="d2", type_=astx.Date(), value=date2 + ) + block.append(date_decl1) + block.append(date_decl2) + block.append(astx.FunctionReturn(astx.LiteralInt32(0))) + + fn = astx.FunctionDef(prototype=proto, body=block) + module.block.append(fn) + + # Should compile successfully + ir_code = builder.translate(module) + assert "2024" in ir_code + assert "2023" in ir_code \ No newline at end of file From d6f42a3e9ad3bfa28da51600371a758054baa343 Mon Sep 17 00:00:00 2001 From: Pratyushanand26 Date: Thu, 23 Oct 2025 06:30:11 +0530 Subject: [PATCH 2/2] fix: resolve linter issues --- src/irx/builders/llvmliteir.py | 56 ++++++++++++++++++++++------------ tests/test_literal_date.py | 36 +++++++++++----------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/irx/builders/llvmliteir.py b/src/irx/builders/llvmliteir.py index 37fa397..028b7f9 100644 --- a/src/irx/builders/llvmliteir.py +++ b/src/irx/builders/llvmliteir.py @@ -19,6 +19,14 @@ from irx.builders.base import Builder, BuilderVisitor from irx.tools.typing import typechecked +DATE_PARTS = 3 +MIN_MONTH = 1 +MAX_MONTH = 12 +MIN_DAY = 1 +MAX_DAY = 31 +MIN_YEAR = 1 +MAX_YEAR = 9999 + @typechecked def safe_pop(lst: list[ir.Value | ir.Function]) -> ir.Value | ir.Function: @@ -45,6 +53,7 @@ class VariablesLLVM: STRING_TYPE: ir.types.Type ASCII_STRING_TYPE: ir.types.Type UTF8_STRING_TYPE: ir.types.Type + DATE_TYPE: ir.types.Type context: ir.context.Context module: ir.module.Module @@ -91,10 +100,6 @@ def get_data_type(self, type_name: str) -> ir.types.Type: return self.VOID_TYPE elif type_name == "date": return self.DATE_TYPE - - # Option 2: store as int64 timestamp (UNIX time) - # return self.INT64_TYPE - raise Exception(f"[EE]: Type name {type_name} not valid.") @@ -139,7 +144,7 @@ def initialize(self) -> None: self._llvm = VariablesLLVM() self._llvm.module = ir.module.Module("Arx") - # ✅ Modern, safe initialization (llvmlite handles most automatically now) + # (llvmlite handles most automatically now) try: llvm.initialize_native_target() llvm.initialize_native_asmprinter() @@ -147,10 +152,10 @@ def initialize(self) -> None: # These may already be initialized — safe to ignore pass - # ✅ Create a new builder for the module + # Create a new builder for the module self._llvm.ir_builder = ir.IRBuilder() - # ✅ Define basic data types + # Define basic data types self._llvm.FLOAT_TYPE = ir.FloatType() self._llvm.FLOAT16_TYPE = ir.HalfType() self._llvm.DOUBLE_TYPE = ir.DoubleType() @@ -165,7 +170,9 @@ def initialize(self) -> None: ) self._llvm.ASCII_STRING_TYPE = ir.IntType(8).as_pointer() self._llvm.UTF8_STRING_TYPE = self._llvm.STRING_TYPE - self._llvm.DATE_TYPE= ir.LiteralStructType([ir.IntType(32),ir.IntType(32),ir.IntType(32)]) + self._llvm.DATE_TYPE = ir.LiteralStructType( + [ir.IntType(32), ir.IntType(32), ir.IntType(32)] + ) def _add_builtins(self) -> None: # The C++ tutorial adds putchard() simply by defining it in the host @@ -669,17 +676,20 @@ def visit(self, node: astx.LiteralDate) -> None: Lower a LiteralDate to LLVM IR. Representation: - { i32 year, i32 month, i32 day } -- emitted as a constant struct. + { i32 year, i32 month, i32 day } + -- emitted as a constant struct. - Expected format: YYYY-MM-DD (ISO), but also accepts single-digit month/day. + Expected format: YYYY-MM-DD (ISO), + but also accepts single-digit month/day. """ s = node.value.strip() # Split by "-" parts = s.split("-") - if len(parts) != 3: + if len(parts) != DATE_PARTS: raise Exception( - f"LiteralDate: invalid date format '{node.value}'. Expected 'YYYY-MM-DD'." + "LiteralDate: invalid date format " + f"'{node.value}'. Expected 'YYYY-MM-DD'." ) try: @@ -693,17 +703,20 @@ def visit(self, node: astx.LiteralDate) -> None: ) # Basic range checks - if not (1 <= month <= 12): + if not (1 <= month <= MAX_MONTH): raise Exception( - f"LiteralDate: month out of range in '{node.value}'. Expected 1-12." + "LiteralDate: month out of range in " + f"'{node.value}'. Expected 1-12." ) - if not (1 <= day <= 31): + if not (1 <= day <= MAX_DAY): raise Exception( - f"LiteralDate: day out of range in '{node.value}'. Expected 1-31." + "LiteralDate: day out of range in " + f"'{node.value}'. Expected 1-31." ) - if not (1 <= year <= 9999): + if not (1 <= year <= MAX_YEAR): raise Exception( - f"LiteralDate: year out of range in '{node.value}'. Expected 1-9999." + "LiteralDate: year out of range in " + f"'{node.value}'. Expected 1-9999." ) # Build constant struct { i32, i32, i32 } @@ -711,12 +724,15 @@ def visit(self, node: astx.LiteralDate) -> None: date_ty = ir.LiteralStructType([i32, i32, i32]) const_date = ir.Constant( date_ty, - [ir.Constant(i32, year), ir.Constant(i32, month), ir.Constant(i32, day)], + [ + ir.Constant(i32, year), + ir.Constant(i32, month), + ir.Constant(i32, day), + ], ) self.result_stack.append(const_date) - @dispatch # type: ignore[no-redef] def visit(self, expr: astx.WhileStmt) -> None: """Translate ASTx While Loop to LLVM-IR.""" diff --git a/tests/test_literal_date.py b/tests/test_literal_date.py index 75dd0f2..e4581dd 100644 --- a/tests/test_literal_date.py +++ b/tests/test_literal_date.py @@ -8,8 +8,6 @@ from irx.builders.base import Builder from irx.builders.llvmliteir import LLVMLiteIR -from .conftest import check_result - @pytest.mark.parametrize( "date_str,expected_year,expected_month,expected_day", @@ -42,16 +40,16 @@ def test_literal_date_basic( name="main", args=astx.Arguments(), return_type=astx.Int32() ) block = astx.Block() - + # Store date in variable date_decl = astx.VariableDeclaration( name="d", type_=astx.Date(), value=date_literal ) block.append(date_decl) - + # Return 0 (just testing that it compiles) block.append(astx.FunctionReturn(astx.LiteralInt32(0))) - + fn = astx.FunctionDef(prototype=proto, body=block) module.block.append(fn) @@ -69,13 +67,13 @@ def test_literal_date_basic( [ ("2024-13-01", "month out of range"), # Invalid month ("2024-00-01", "month out of range"), # Month = 0 - ("2024-01-32", "day out of range"), # Invalid day - ("2024-01-00", "day out of range"), # Day = 0 + ("2024-01-32", "day out of range"), # Invalid day + ("2024-01-00", "day out of range"), # Day = 0 ("10000-01-01", "year out of range"), # Year too large - ("0-01-01", "year out of range"), # Year = 0 - ("2024/01/01", "invalid date format"), # Wrong separator - ("2024-Jan-01", "invalid year/month/day"), # Text month - ("2024-01", "invalid date format"), # Missing day + ("0-01-01", "year out of range"), # Year = 0 + ("2024/01/01", "invalid date format"), # Wrong separator + ("2024-Jan-01", "invalid year/month/day"), # Text month + ("2024-01", "invalid date format"), # Missing day ], ) @pytest.mark.parametrize("builder_class", [LLVMLiteIR]) @@ -94,20 +92,20 @@ def test_literal_date_invalid( name="main", args=astx.Arguments(), return_type=astx.Int32() ) block = astx.Block() - + date_decl = astx.VariableDeclaration( name="d", type_=astx.Date(), value=date_literal ) block.append(date_decl) block.append(astx.FunctionReturn(astx.LiteralInt32(0))) - + fn = astx.FunctionDef(prototype=proto, body=block) module.block.append(fn) # Should raise an exception with expected message with pytest.raises(Exception) as exc_info: builder.translate(module) - + assert error_msg in str(exc_info.value).lower() @@ -134,13 +132,13 @@ def test_literal_date_edge_cases( name="main", args=astx.Arguments(), return_type=astx.Int32() ) block = astx.Block() - + date_decl = astx.VariableDeclaration( name="d", type_=astx.Date(), value=date_literal ) block.append(date_decl) block.append(astx.FunctionReturn(astx.LiteralInt32(0))) - + fn = astx.FunctionDef(prototype=proto, body=block) module.block.append(fn) @@ -164,7 +162,7 @@ def test_literal_date_multiple_variables( name="main", args=astx.Arguments(), return_type=astx.Int32() ) block = astx.Block() - + # Store multiple dates date_decl1 = astx.VariableDeclaration( name="d1", type_=astx.Date(), value=date1 @@ -175,11 +173,11 @@ def test_literal_date_multiple_variables( block.append(date_decl1) block.append(date_decl2) block.append(astx.FunctionReturn(astx.LiteralInt32(0))) - + fn = astx.FunctionDef(prototype=proto, body=block) module.block.append(fn) # Should compile successfully ir_code = builder.translate(module) assert "2024" in ir_code - assert "2023" in ir_code \ No newline at end of file + assert "2023" in ir_code