diff --git a/.makim.yaml b/.makim.yaml index 4d4a92b..bfcb8b3 100644 --- a/.makim.yaml +++ b/.makim.yaml @@ -85,9 +85,20 @@ groups: type: string required: true run: | + import subprocess + for name in "${{ args.examples }}".split(","): print(f" show tokens: {name} ".center(80, "=")) - arx --show-tokens examples/@(name).x + result = subprocess.run( + ["arx", "--show-tokens", f"examples/{name}.x"], + capture_output=True, + text=True, + check=True, + ) + output = result.stdout.strip() + if not output: + raise RuntimeError(f"show-tokens produced no output for: {name}") + print(output) show-ast: help: Emit ast for input file args: @@ -96,9 +107,25 @@ groups: type: string required: true run: | + import subprocess + for name in "${{ args.examples }}".split(","): print(f" show ast: {name} ".center(80, "=")) - arx --show-ast examples/@(name).x + result = subprocess.run( + ["arx", "--show-ast", f"examples/{name}.x"], + capture_output=True, + text=True, + check=True, + ) + output = result.stdout.strip() + if not output: + raise RuntimeError(f"show-ast produced no output for: {name}") + if output == "Block": + raise RuntimeError( + "show-ast regression: got only 'Block'. " + f"example={name}" + ) + print(output) show-llvm-ir: help: Emit ast for input file args: @@ -107,9 +134,23 @@ groups: type: string required: true run: | + import subprocess + for name in "${{ args.examples }}".split(","): print(f" show llvm ir: {name} ".center(80, "=")) - arx --show-llvm-ir examples/@(name).x + result = subprocess.run( + ["arx", "--show-llvm-ir", f"examples/{name}.x"], + capture_output=True, + text=True, + check=True, + ) + output = result.stdout.strip() + if "; ModuleID" not in output: + raise RuntimeError( + "show-llvm-ir output missing LLVM header for: " + f"{name}" + ) + print(output) emit-object: help: Emit ast for input file args: @@ -118,9 +159,34 @@ groups: type: string required: true run: | + import subprocess + + from pathlib import Path + + output_dir = Path("build/smoke") + output_dir.mkdir(parents=True, exist_ok=True) + for name in "${{ args.examples }}".split(","): print(f" emit object: {name} ".center(80, "=")) - arx examples/@(name).x + output_file = output_dir / name + if output_file.exists(): + output_file.unlink() + subprocess.run( + [ + "arx", + f"examples/{name}.x", + "--lib", + "--output-file", + str(output_file), + ], + check=True, + ) + if not output_file.exists() or output_file.stat().st_size == 0: + raise RuntimeError( + "emit-object did not produce a valid artifact for: " + f"{name}" + ) + print(str(output_file)) docs: tasks: diff --git a/AGENTS.md b/AGENTS.md index 2855625..ecd5855 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,9 +94,11 @@ Current language behavior (from parser/lexer/tests/syntax manifest): - Numeric literals: decimal integer/float (single `.` max) - String/char/bool/none literals are supported - Comments: `#` line comments -- Function definitions: `fn name(arg: type, ...)` followed by indented block +- Function definitions: `fn name(arg: type, ...) -> type` followed by indented + block - Function arguments must be explicitly typed -- Extern definitions: `extern name(arg: type, ...)` +- Variable declarations must be explicitly typed (`var name: type`) +- Extern definitions: `extern name(arg: type, ...) -> type` - Control flow: `if/else`, `while`, `for ... in (...)`, count-style `for` - Range-style for header is `(start:end:step)` (tuple-style is rejected) - Builtins: `cast(value, type)` and `print(expr)` diff --git a/docs/getting-started.md b/docs/getting-started.md index e20de22..fcdd888 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -273,13 +273,13 @@ Variables are declared with `var`: title: Variable example summary: Shows var binding inside a function. ``` -fn example(): +fn example() -> i32: ``` title: example summary: Binds a variable and computes a result. ``` - var a = 10 in - a + 1 + var a: i32 = 10 + return a + 1 ```` ### Extern Functions diff --git a/docs/library/datatypes.md b/docs/library/datatypes.md index 7617c70..4bc41a9 100644 --- a/docs/library/datatypes.md +++ b/docs/library/datatypes.md @@ -1,13 +1,13 @@ # Data Types Arx uses explicit type annotations for variables, function parameters, and -optional function return types. +function return types. ## Type Annotations - Function parameters must always be typed. -- Function return type is optional. If omitted, current parser default is `f32`. -- Variable declarations can include an explicit type with `var name: type`. +- Function return type must always be explicit with `-> type`. +- Variable declarations must include an explicit type with `var name: type`. ````arx ``` diff --git a/docs/library/docstrings.md b/docs/library/docstrings.md index a7565db..98f6f3f 100644 --- a/docs/library/docstrings.md +++ b/docs/library/docstrings.md @@ -45,7 +45,7 @@ Valid: ``` title: Module docs ``` -fn main(): +fn main() -> i32: ``` title: main summary: Entry point for the module. @@ -61,7 +61,7 @@ after `:` and the required newline/indentation. Valid: ````text -fn main(): +fn main() -> i32: ``` title: Function docs summary: Function summary @@ -72,7 +72,7 @@ fn main(): Invalid: ````text -fn main(): +fn main() -> i32: return 1 ``` title: Too late diff --git a/docs/library/functions.md b/docs/library/functions.md index 75fd600..ec53af7 100644 --- a/docs/library/functions.md +++ b/docs/library/functions.md @@ -32,7 +32,7 @@ fn double(v: i32) -> i32: ``` return v * 2 -fn main(): +fn main() -> i32: ``` title: main summary: Calls double with a constant input. diff --git a/docs/library/modules.md b/docs/library/modules.md index 9058ad5..8d91040 100644 --- a/docs/library/modules.md +++ b/docs/library/modules.md @@ -41,7 +41,7 @@ Valid: ``` title: Module docs ``` -fn main(): +fn main() -> i32: ``` title: main summary: Entry point for the module. @@ -55,7 +55,7 @@ Invalid (leading indentation before module docstring): ``` title: Module docs ``` -fn main(): +fn main() -> i32: ``` title: main summary: Entry point for the module. diff --git a/src/arx/parser.py b/src/arx/parser.py index 7d590e1..1060322 100644 --- a/src/arx/parser.py +++ b/src/arx/parser.py @@ -596,10 +596,14 @@ def parse_inline_var_declaration(self) -> astx.InlineVariableDeclaration: name = cast(str, self.tokens.cur_tok.value) self.tokens.get_next_token() # eat identifier - var_type: astx.DataType = astx.Float32() - if self._is_operator(":"): - self._consume_operator(":") - var_type = self.parse_type() + if not self._is_operator(":"): + raise ParserException( + "Parser: Expected type annotation for inline variable " + f"'{name}'." + ) + + self._consume_operator(":") + var_type = self.parse_type() self._consume_operator("=") value = self.parse_expression() @@ -626,10 +630,13 @@ def parse_var_expr(self) -> astx.VariableDeclaration: name = cast(str, self.tokens.cur_tok.value) self.tokens.get_next_token() # eat identifier - var_type: astx.DataType = astx.Float32() - if self._is_operator(":"): - self._consume_operator(":") - var_type = self.parse_type() + if not self._is_operator(":"): + raise ParserException( + f"Parser: Expected type annotation for variable '{name}'." + ) + + self._consume_operator(":") + var_type = self.parse_type() value: astx.Expr | None = None if self._is_operator("="): @@ -824,10 +831,13 @@ def parse_prototype(self, expect_colon: bool) -> astx.FunctionPrototype: self._consume_operator(")") - ret_type: astx.DataType = astx.Float32() - if self._is_operator("->"): - self._consume_operator("->") - ret_type = self.parse_type() + if not self._is_operator("->"): + raise ParserException( + "Parser: Expected return type annotation with '->'." + ) + + self._consume_operator("->") + ret_type = self.parse_type() if expect_colon: self._consume_operator(":") diff --git a/tests/test_app_paths.py b/tests/test_app_paths.py index 3e52048..744ff00 100644 --- a/tests/test_app_paths.py +++ b/tests/test_app_paths.py @@ -105,7 +105,7 @@ def test_arxio_file_and_stdin_loaders( type: Path """ sample = tmp_path / "sample.x" - sample.write_text("fn main():\n return 1\n", encoding="utf-8") + sample.write_text("fn main() -> i32:\n return 1\n", encoding="utf-8") ArxIO.file_to_buffer(str(sample)) assert "fn main()" in ArxIO.buffer.buffer diff --git a/tests/test_codegen_ast_output.py b/tests/test_codegen_ast_output.py index 53d9eb3..411187b 100644 --- a/tests/test_codegen_ast_output.py +++ b/tests/test_codegen_ast_output.py @@ -13,8 +13,8 @@ @pytest.mark.parametrize( "code", [ - "fn main():\n return 0.0 + 1.0", - "fn main():\n return 1.0 + 2.0 * (3.0 - 2.0)", + "fn main() -> f32:\n return 0.0 + 1.0", + "fn main() -> f32:\n return 1.0 + 2.0 * (3.0 - 2.0)", "fn main() -> i32:\n print(42)\n return 0", "fn main() -> i32:\n print(3.5)\n return 0", ( diff --git a/tests/test_codegen_file_object.py b/tests/test_codegen_file_object.py index 5065838..afb2bd8 100644 --- a/tests/test_codegen_file_object.py +++ b/tests/test_codegen_file_object.py @@ -21,8 +21,8 @@ @pytest.mark.parametrize( "code", [ - "fn main():\n return 1.0 + 1.0", - "fn main():\n return 1.0 + 2.0 * (3.0 - 2.0)", + "fn main() -> f32:\n return 1.0 + 1.0", + "fn main() -> f32:\n return 1.0 + 2.0 * (3.0 - 2.0)", # "fn main():\n if (1 < 2):\n return 3\nelse:\n return 2\n", ], ) diff --git a/tests/test_parser.py b/tests/test_parser.py index d9f24a9..f6b2c83 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -87,7 +87,7 @@ def test_parse() -> None: expr = parser.parse(lexer.lex()) assert expr - assert isinstance(expr, astx.Block) + assert isinstance(expr, astx.Module) def test_parse_if_stmt() -> None: @@ -117,7 +117,7 @@ def test_parse_fn() -> None: title: Test gettok for main tokens. """ ArxIO.string_to_buffer( - "fn math(x: i32):\n" + "fn math(x: i32) -> i32:\n" + " if 1 > 2:\n" + " a = 1\n" + " else:\n" @@ -156,7 +156,7 @@ def test_parse_module_docstring() -> None: "title: Module docs\n" "summary: Main module reference\n" "```\n" - "fn main():\n" + "fn main() -> i32:\n" " return 1\n" ) @@ -164,7 +164,7 @@ def test_parse_module_docstring() -> None: parser = Parser() tree = parser.parse(lexer.lex()) - assert isinstance(tree, astx.Block) + assert isinstance(tree, astx.Module) assert len(tree.nodes) == 1 assert isinstance(tree.nodes[0], astx.FunctionDef) @@ -174,7 +174,7 @@ def test_parse_module_docstring_must_start_first_line() -> None: title: Test module docstring must start at line 1, column 1. """ ArxIO.string_to_buffer( - " ```\n title: module docs\n ```\nfn main():\n return 1\n" + " ```\n title: module docs\n ```\nfn main() -> i32:\n return 1\n" ) lexer = Lexer() @@ -189,7 +189,7 @@ def test_parse_function_docstring() -> None: title: Test function docstring as first body statement. """ ArxIO.string_to_buffer( - "fn main():\n" + "fn main() -> i32:\n" " ```\n" " title: Function docs\n" " summary: Function summary\n" @@ -213,7 +213,7 @@ def test_parse_function_docstring_must_be_first_stmt() -> None: title: Test function docstring invalid placement after expressions. """ ArxIO.string_to_buffer( - "fn main():\n return 1\n ```\n title: Function docs\n ```\n" + "fn main() -> i32:\n return 1\n ```\n title: Function docs\n ```\n" ) lexer = Lexer() @@ -228,7 +228,9 @@ def test_parse_module_docstring_invalid_douki_schema() -> None: title: Test module docstring validation against Douki schema. """ ArxIO.string_to_buffer( - "```\nsummary: Missing required title\n```\nfn main():\n return 1\n" + "```\nsummary: Missing required title\n```\n" + "fn main() -> i32:\n" + " return 1\n" ) lexer = Lexer() @@ -243,7 +245,7 @@ def test_parse_function_docstring_invalid_douki_schema() -> None: title: Test function docstring validation against Douki schema. """ ArxIO.string_to_buffer( - "fn main():\n" + "fn main() -> i32:\n" " ```\n" " bad_field: this is not allowed by schema\n" " ```\n" @@ -283,7 +285,7 @@ def test_parse_while_stmt() -> None: title: Test while statement parsing. """ ArxIO.string_to_buffer( - "fn main():\n" + "fn main() -> i32:\n" " var a: i32 = 0\n" " while a < 10:\n" " a = a + 1\n" @@ -303,7 +305,9 @@ def test_parse_for_count_stmt() -> None: title: Test count-style for parsing. """ ArxIO.string_to_buffer( - "fn main():\n for var i: i32 = 0; i < 5; i = i + 1:\n return i\n" + "fn main() -> i32:\n" + " for var i: i32 = 0; i < 5; i = i + 1:\n" + " return i\n" ) lexer = Lexer() parser = Parser() @@ -318,7 +322,9 @@ def test_parse_for_range_slice_style() -> None: """ title: Test range-style for parsing with colon-separated bounds. """ - ArxIO.string_to_buffer("fn main():\n for j in (0:5:1):\n return j\n") + ArxIO.string_to_buffer( + "fn main() -> i32:\n for j in (0:5:1):\n return j\n" + ) lexer = Lexer() parser = Parser() @@ -332,7 +338,9 @@ def test_parse_for_range_tuple_style_is_rejected() -> None: """ title: Tuple-style for range must be rejected. """ - ArxIO.string_to_buffer("fn main():\n for j in (0, 5, 1):\n return j\n") + ArxIO.string_to_buffer( + "fn main() -> i32:\n for j in (0, 5, 1):\n return j\n" + ) lexer = Lexer() parser = Parser() @@ -345,7 +353,10 @@ def test_parse_builtin_cast_and_print() -> None: title: Test builtin cast and print node generation. """ ArxIO.string_to_buffer( - "fn main():\n var a: i32 = 1\n print(cast(a, str))\n return a\n" + "fn main() -> i32:\n" + " var a: i32 = 1\n" + " print(cast(a, str))\n" + " return a\n" ) lexer = Lexer() parser = Parser() @@ -361,7 +372,7 @@ def test_parse_block_with_comment_and_blank_lines() -> None: title: Test block parsing across comment/blank lines. """ ArxIO.string_to_buffer( - "fn main():\n" + "fn main() -> i32:\n" " # section A\n" "\n" " var a: i32 = 1\n" diff --git a/tests/test_parser_branches.py b/tests/test_parser_branches.py index d0b606b..46a9ceb 100644 --- a/tests/test_parser_branches.py +++ b/tests/test_parser_branches.py @@ -13,7 +13,7 @@ from arx.parser import Parser -def _parse(code: str) -> astx.Block: +def _parse(code: str) -> astx.Module: ArxIO.string_to_buffer(code) return Parser().parse(Lexer().lex()) @@ -23,7 +23,7 @@ def test_parse_literal_kinds_and_list_literal() -> None: title: Parse string/char/bool/none and list literals. """ tree = _parse( - "fn main():\n" + "fn main() -> list[i32]:\n" ' var s: str = "abc"\n' " var c: char = 'A'\n" " var b: bool = true\n" @@ -49,7 +49,7 @@ def test_parse_list_literal_rejects_non_literal_elements() -> None: r"Unknown token when expecting an expression)" ), ): - _parse("fn main():\n return [foo]\n") + _parse("fn main() -> list[i32]:\n return [foo]\n") def test_parse_datetime_and_timestamp_builtins() -> None: @@ -57,9 +57,9 @@ def test_parse_datetime_and_timestamp_builtins() -> None: title: Parse datetime/timestamp builtin literals. """ tree = _parse( - "fn dt():\n" + "fn dt() -> datetime:\n" ' return datetime("2026-01-01T00:00:00")\n' - "fn ts():\n" + "fn ts() -> timestamp:\n" ' return timestamp("2026-01-01T00:00:00Z")\n' ) @@ -78,14 +78,20 @@ def test_parse_datetime_requires_string_literal() -> None: title: Datetime builtin expects a string literal. """ with pytest.raises(ParserException, match="expects a string literal"): - _parse("fn main():\n return datetime(1)\n") + _parse("fn main() -> datetime:\n return datetime(1)\n") @pytest.mark.parametrize( "code, expected", [ - ("fn main():\n for 1 in (0:1:1):\n return 1\n", "identifier"), - ("fn main():\n for i (0:1:1):\n return i\n", "Expected 'in'"), + ( + "fn main() -> i32:\n for 1 in (0:1:1):\n return 1\n", + "identifier", + ), + ( + "fn main() -> i32:\n for i (0:1:1):\n return i\n", + "Expected 'in'", + ), ], ) def test_parse_for_error_paths(code: str, expected: str) -> None: @@ -117,6 +123,12 @@ def test_parse_inline_var_declaration_error_paths() -> None: with pytest.raises(ParserException, match="identifier after var"): parser.parse_inline_var_declaration() + ArxIO.string_to_buffer("var i = 0") + parser = Parser(Lexer().lex()) + parser.tokens.get_next_token() + with pytest.raises(ParserException, match="type annotation"): + parser.parse_inline_var_declaration() + def test_parse_var_expr_error_paths() -> None: """ @@ -128,12 +140,18 @@ def test_parse_var_expr_error_paths() -> None: with pytest.raises(ParserException, match="identifier after var"): parser.parse_var_expr() - ArxIO.string_to_buffer("var a in") + ArxIO.string_to_buffer("var a: i32 in") parser = Parser(Lexer().lex()) parser.tokens.get_next_token() with pytest.raises(ParserException, match="Legacy 'var"): parser.parse_var_expr() + ArxIO.string_to_buffer("var a = 1") + parser = Parser(Lexer().lex()) + parser.tokens.get_next_token() + with pytest.raises(ParserException, match="type annotation"): + parser.parse_var_expr() + def test_parse_type_error_paths() -> None: """ @@ -221,6 +239,12 @@ def test_parse_unary_and_prototype_error_paths() -> None: with pytest.raises(ParserException, match="Expected type annotation"): parser.parse_prototype(expect_colon=False) + ArxIO.string_to_buffer("f(x: i32)") + parser = Parser(Lexer().lex()) + parser.tokens.get_next_token() + with pytest.raises(ParserException, match="Expected return type"): + parser.parse_prototype(expect_colon=False) + def test_parse_block_error_paths() -> None: """ @@ -241,7 +265,7 @@ def test_parse_block_error_paths() -> None: parser.parse_block() with pytest.raises(ParserException, match="Indentation not allowed here"): - _parse("fn main():\n a = 1\n b = 2\n") + _parse("fn main() -> i32:\n a = 1\n b = 2\n") def test_parse_primary_unknown_token_branch() -> None: