├── docs ├── CNAME ├── pong_preview.png ├── advanced │ ├── index.md │ ├── type-definition.md │ ├── toolchain-architecture.md │ ├── typechecker.md │ └── ffi.md ├── tutorial │ ├── bitwise-operators.md │ ├── index.md │ ├── typecasts.md │ ├── stack-management-operators.md │ └── structures.md ├── datatypes │ └── string.md ├── installation.md └── index.md ├── editors └── vscode │ ├── .gitignore │ ├── package.json │ └── syntaxes │ └── gofra.tmLanguage.json ├── examples ├── .gitignore ├── 01_hello_world.gof ├── 02_input.gof ├── 02_fib.gof ├── 02_cat.gof └── 04_http_server.gof ├── tests ├── .gitignore ├── test_string_view_struct_accesor.gof ├── test_math_pow.gof ├── test_math_sqrt.gof ├── test_math_factorial.gof ├── test_global_static_variable_zero_initialized.gof ├── test_math_abs.gof ├── test_math_min_max.gof ├── test_array_global_single_element.gof ├── test_array_local_single_element.gof ├── test_struct_static_initializer.gof ├── test_generics_identity_type.gof ├── test_for_in_iterable_sum.gof ├── test_math_clamp.gof ├── test_while_iterations.gof ├── test_for_in_range_sum.gof ├── test_std_random.gof ├── test_sizeof_char_array.gof ├── test_abi_stack_spill_arguments.gof ├── test_struct_simple_rw.gof ├── test_while_nested_counter.gof ├── test_static_struct_passed_by_reference.gof ├── test_characters_as_integer.gof ├── test_inline_asm_exit_darwin_arm.gof ├── test_numerical_literals.gof ├── test_array_local_massive_blob.gof ├── test_variable_default_integer_value.gof ├── test_struct_field_type_forward_reference.gof ├── test_variables_rw.gof ├── test_boolean_logic_operators.gof └── test_array_character_word.gof ├── gofra ├── cli │ ├── parser │ │ ├── __init__.py │ │ ├── builder.py │ │ └── arguments.py │ ├── errors │ │ ├── __init__.py │ │ └── error_handler.py │ ├── __main__.py │ ├── __init__.py │ ├── goals │ │ ├── _optimization_pipeline.py │ │ ├── version.py │ │ ├── __init__.py │ │ └── preprocessor.py │ ├── main.py │ ├── output.py │ └── readline.py ├── testkit │ ├── cli │ │ ├── __init__.py │ │ └── matrix.py │ ├── __init__.py │ ├── __main__.py │ └── test.py ├── cache │ ├── __init__.py │ ├── vcs.py │ └── directory.py ├── __init__.py ├── __main__.py ├── execution │ ├── __init__.py │ ├── permissions.py │ └── execution.py └── executable.py ├── libgofra ├── lexer │ ├── tests │ │ ├── __init__.py │ │ ├── test_string_literals.py │ │ └── test_character_literals.py │ ├── literals │ │ ├── __init__.py │ │ ├── string.py │ │ └── character.py │ ├── io │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── io.py │ ├── errors │ │ ├── empty_character_literal.py │ │ ├── unclosed_string_quote.py │ │ ├── unclosed_character_quote.py │ │ ├── __init__.py │ │ ├── ambiguous_hexadecimal_alphabet.py │ │ └── excessive_character_length.py │ ├── __init__.py │ ├── _state.py │ └── helpers.py ├── linker │ ├── __init__.py │ ├── gnu │ │ ├── __init__.py │ │ └── target_formats.py │ ├── entry_point.py │ ├── pkgconfig │ │ ├── __init__.py │ │ └── pkgconfig.py │ ├── output_format.py │ ├── profile.py │ ├── apple │ │ ├── platforms.py │ │ ├── __init__.py │ │ ├── architectures.py │ │ ├── libraries.py │ │ └── output_format.py │ ├── command_composer.py │ └── linker.py ├── parser │ ├── errors │ │ ├── __init__.py │ │ ├── general_expect_token.py │ │ ├── top_level_expected_no_operators.py │ │ ├── type_definition_already_exists.py │ │ ├── top_level_keyword_in_local_scope.py │ │ ├── local_level_keyword_in_global_scope.py │ │ ├── unknown_primitive_type.py │ │ ├── keyword_in_without_loop_block.py │ │ ├── cannot_infer_var_type_from_initializer.py │ │ ├── unknown_field_accessor_struct_field.py │ │ ├── constant_variable_requires_initializer.py │ │ ├── cannot_infer_var_type_from_empty_array_initializer.py │ │ ├── variable_with_void_type.py │ │ └── type_has_no_compile_time_initializer.py │ ├── functions │ │ └── __init__.py │ ├── __init__.py │ ├── typecast.py │ ├── operators.py │ ├── typedef.py │ └── runtime_oob_check.py ├── codegen │ ├── backends │ │ ├── aarch64 │ │ │ ├── __init__.py │ │ │ ├── sections │ │ │ │ ├── __init__.py │ │ │ │ ├── _alignment.py │ │ │ │ ├── text_strings.py │ │ │ │ └── bss_variables.py │ │ │ ├── _context.py │ │ │ ├── registers.py │ │ │ └── static_data_section.py │ │ ├── amd64 │ │ │ ├── __init__.py │ │ │ ├── registers.py │ │ │ ├── frame.py │ │ │ └── _context.py │ │ ├── general.py │ │ ├── __init__.py │ │ ├── alignment.py │ │ └── base.py │ ├── sections │ │ ├── __init__.py │ │ ├── _factory.py │ │ ├── elf.py │ │ └── macho.py │ ├── __init__.py │ ├── get_backend.py │ └── generator.py ├── consts.py ├── optimizer │ ├── strategies │ │ ├── __init__.py │ │ └── dead_code_elimination.py │ ├── helpers │ │ ├── __init__.py │ │ └── function_usage.py │ ├── __init__.py │ ├── config.py │ └── pipeline.py ├── __init__.py ├── targets │ ├── __init__.py │ └── infer_host.py ├── preprocessor │ ├── include │ │ ├── __init__.py │ │ └── distribution.py │ ├── __init__.py │ ├── macros │ │ ├── defaults.py │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── macro.py │ ├── exceptions.py │ ├── conditions │ │ ├── exceptions.py │ │ └── __init__.py │ └── _state.py ├── typecheck │ ├── __init__.py │ ├── errors │ │ ├── __init__.py │ │ ├── user_defined_compile_time_error.py │ │ ├── no_main_entry_function.py │ │ ├── return_value_missing.py │ │ ├── entry_point_return_type_mismatch.py │ │ ├── entry_point_parameters_mismatch.py │ │ ├── parameter_type_mismatch.py │ │ └── missing_function_argument.py │ └── entry_point.py ├── types │ ├── composite │ │ ├── __init__.py │ │ ├── function.py │ │ ├── pointer.py │ │ ├── array.py │ │ ├── structure.py │ │ └── string.py │ ├── primitive │ │ ├── __init__.py │ │ ├── character.py │ │ ├── integers.py │ │ ├── floats.py │ │ ├── boolean.py │ │ └── void.py │ ├── _base.py │ ├── __init__.py │ └── registry.py ├── hir │ ├── __init__.py │ ├── initializer.py │ └── module.py ├── exceptions.py ├── feature_flags.py ├── assembler │ ├── assembler.py │ ├── __init__.py │ └── clang.py └── gofra.py ├── scripts ├── __init__.py ├── run_acceptance_tests.sh ├── build.py └── macos_syscall_extractor.py ├── .gitattributes ├── lib ├── http │ └── http.gof ├── os │ ├── io │ │ ├── io.gof │ │ └── std_handles.gof │ ├── os.gof │ ├── general.gof │ ├── time.gof │ └── process.gof ├── memory.gof ├── ffi.gof ├── std.gof ├── types.gof ├── assert.gof ├── random.gof ├── math.gof └── string.gof ├── .gitignore ├── mkdocs.yml ├── .vscode └── launch.json ├── LICENSE ├── .github └── workflows │ └── testkit.yml └── pyproject.toml /docs/CNAME: -------------------------------------------------------------------------------- 1 | gofralang.ru -------------------------------------------------------------------------------- /editors/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | *.vsix -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | ![0-9]*.gof 3 | !.gitignore -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !test_*.gof 3 | !.gitignore -------------------------------------------------------------------------------- /gofra/cli/parser/__init__.py: -------------------------------------------------------------------------------- 1 | """CLI parser of arguments.""" 2 | -------------------------------------------------------------------------------- /gofra/testkit/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """CLI interface for testkit.""" 2 | -------------------------------------------------------------------------------- /libgofra/lexer/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for lexer package.""" 2 | -------------------------------------------------------------------------------- /libgofra/linker/__init__.py: -------------------------------------------------------------------------------- 1 | """Linker for Gofra object files.""" 2 | -------------------------------------------------------------------------------- /gofra/cache/__init__.py: -------------------------------------------------------------------------------- 1 | """Cache helper logic for toolchain processing.""" 2 | -------------------------------------------------------------------------------- /gofra/cli/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Error display and handler for Gofra.""" 2 | -------------------------------------------------------------------------------- /libgofra/linker/gnu/__init__.py: -------------------------------------------------------------------------------- 1 | """GNU Linker (ld on Linux systems).""" 2 | -------------------------------------------------------------------------------- /libgofra/parser/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Parser errors for language users.""" 2 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """Different scripts / tools for Gofra development.""" 2 | -------------------------------------------------------------------------------- /libgofra/parser/functions/__init__.py: -------------------------------------------------------------------------------- 1 | """Parser for functions in language.""" 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/__init__.py: -------------------------------------------------------------------------------- 1 | """AARCH64 code generation backend.""" 2 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/amd64/__init__.py: -------------------------------------------------------------------------------- 1 | """AMD64 (x86_64) code generation backend.""" 2 | -------------------------------------------------------------------------------- /libgofra/consts.py: -------------------------------------------------------------------------------- 1 | """Constants used inside Gofra.""" 2 | 3 | GOFRA_ENTRY_POINT = "main" 4 | -------------------------------------------------------------------------------- /docs/pong_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillzhosul/gofra/HEAD/docs/pong_preview.png -------------------------------------------------------------------------------- /libgofra/parser/__init__.py: -------------------------------------------------------------------------------- 1 | """Parser package that used to parse source tokens into operators.""" 2 | -------------------------------------------------------------------------------- /libgofra/optimizer/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | """Optimization strategies applied to the program to optimize it.""" 2 | -------------------------------------------------------------------------------- /libgofra/__init__.py: -------------------------------------------------------------------------------- 1 | """Gofra programming language. 2 | 3 | Provides toolchain including CLI, compiler etc. 4 | """ 5 | -------------------------------------------------------------------------------- /libgofra/linker/entry_point.py: -------------------------------------------------------------------------------- 1 | # That symbol is expected by linker from object to link against for executables 2 | LINKER_EXPECTED_ENTRY_POINT = "_start" 3 | -------------------------------------------------------------------------------- /gofra/cli/__main__.py: -------------------------------------------------------------------------------- 1 | """Entry point for toolchain CLI.""" 2 | 3 | from .main import cli_entry_point 4 | 5 | if __name__ == "__main__": 6 | cli_entry_point() 7 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/sections/__init__.py: -------------------------------------------------------------------------------- 1 | """Assembly section specification, writers and etc for different object file formats (ELF/COFF/MACHO).""" 2 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/general.py: -------------------------------------------------------------------------------- 1 | """General consts and types for registers and architecture (including FFI/ABI/IPC).""" 2 | 3 | CODEGEN_GOFRA_CONTEXT_LABEL = ".L_%s_%s" 4 | -------------------------------------------------------------------------------- /libgofra/targets/__init__.py: -------------------------------------------------------------------------------- 1 | """Target specifications for build process (e.g target triple but improved).""" 2 | 3 | from .target import Target 4 | 5 | __all__ = ("Target",) 6 | -------------------------------------------------------------------------------- /gofra/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """Command-Line-Interface (CLI) for Gofra. 2 | 3 | Core toolchain provider for Gofra, also contains some useful functions for other CLIs (e.g testkit). 4 | """ 5 | -------------------------------------------------------------------------------- /libgofra/optimizer/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Helper utilities for optimizations.""" 2 | 3 | from .function_usage import is_function_has_callers 4 | 5 | __all__ = ("is_function_has_callers",) 6 | -------------------------------------------------------------------------------- /lib/http/http.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // HTTP client/server interaction 3 | // ============================================== 4 | 5 | #include "http_server.gof" -------------------------------------------------------------------------------- /gofra/testkit/__init__.py: -------------------------------------------------------------------------------- 1 | """Test system for internal usage by language developers. 2 | 3 | Currently (or ever) does not expected to be used as language-user test system. 4 | """ 5 | 6 | __all__ = () 7 | -------------------------------------------------------------------------------- /libgofra/linker/pkgconfig/__init__.py: -------------------------------------------------------------------------------- 1 | """`pkg-config` wrapper for searching for library paths. 2 | 3 | Component is not used if `pkg-config` is not present on system or specified to not use it. 4 | """ 5 | -------------------------------------------------------------------------------- /libgofra/preprocessor/include/__init__.py: -------------------------------------------------------------------------------- 1 | """Include system for preprocessor.""" 2 | 3 | from .resolver import resolve_include_from_token_into_state 4 | 5 | __all__ = ("resolve_include_from_token_into_state",) 6 | -------------------------------------------------------------------------------- /tests/test_string_view_struct_accesor.gof: -------------------------------------------------------------------------------- 1 | /// Tests using string-view struct as accessor 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 8 4 | 5 | var str = "12345678" 6 | 7 | func int main[] 8 | str.len return 9 | end -------------------------------------------------------------------------------- /tests/test_math_pow.gof: -------------------------------------------------------------------------------- 1 | /// Test power function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 81 6 | 7 | func int main[] 8 | 3 4 pow return // 3ˆ4 => 81 9 | end -------------------------------------------------------------------------------- /tests/test_math_sqrt.gof: -------------------------------------------------------------------------------- 1 | /// Test square root function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 23 6 | 7 | func int main[] 8 | 529 sqrt return // √529 = 23 9 | end -------------------------------------------------------------------------------- /gofra/__init__.py: -------------------------------------------------------------------------------- 1 | """Gofra programming language toolchain. 2 | 3 | Provides CLI, REPL and other stuff. 4 | Depends on `libgofra` - core compiler package. 5 | 6 | This package does not contain any implementation of the compiler itself! 7 | """ 8 | -------------------------------------------------------------------------------- /libgofra/typecheck/__init__.py: -------------------------------------------------------------------------------- 1 | """Typecheck (type safety) package. 2 | 3 | Provides validation for type system and resulting behavior 4 | """ 5 | 6 | from .typechecker import validate_type_safety 7 | 8 | __all__ = ["validate_type_safety"] 9 | -------------------------------------------------------------------------------- /lib/os/io/io.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // I/O (Input / Output) manipulations based on operating system (wrapper) 3 | // ============================================== 4 | 5 | #include "output.gof" 6 | #include "input.gof" -------------------------------------------------------------------------------- /libgofra/types/composite/__init__.py: -------------------------------------------------------------------------------- 1 | """Composite types for type system.""" 2 | 3 | from .array import ArrayType 4 | from .function import FunctionType 5 | from .pointer import PointerType 6 | 7 | __all__ = ("ArrayType", "FunctionType", "PointerType") 8 | -------------------------------------------------------------------------------- /tests/test_math_factorial.gof: -------------------------------------------------------------------------------- 1 | /// Test min value function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 120 6 | 7 | func int main[] 8 | 5 factorial 9 | return // 5! => 120 10 | end -------------------------------------------------------------------------------- /gofra/__main__.py: -------------------------------------------------------------------------------- 1 | """Entry point for CLI. 2 | 3 | Only for calling via `python -m gofra`, which is considered as bad practice. 4 | """ 5 | 6 | from gofra.cli.main import cli_entry_point 7 | 8 | if __name__ == "__main__": 9 | cli_entry_point() 10 | -------------------------------------------------------------------------------- /libgofra/codegen/sections/__init__.py: -------------------------------------------------------------------------------- 1 | """Assembly section specification for different object file formats (ELF/COFF/MACHO).""" 2 | 3 | from ._factory import SectionType, get_os_assembler_section 4 | 5 | __all__ = ["SectionType", "get_os_assembler_section"] 6 | -------------------------------------------------------------------------------- /libgofra/lexer/literals/__init__.py: -------------------------------------------------------------------------------- 1 | """Tokenizers for literal symbols (tokens).""" 2 | 3 | from .character import tokenize_character_literal 4 | from .string import tokenize_string_literal 5 | 6 | __all__ = ["tokenize_character_literal", "tokenize_string_literal"] 7 | -------------------------------------------------------------------------------- /tests/test_global_static_variable_zero_initialized.gof: -------------------------------------------------------------------------------- 1 | /// Test that static global variables are zero-initialized 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 0 5 | 6 | var static int; 7 | 8 | func int main[] 9 | static return 10 | end -------------------------------------------------------------------------------- /libgofra/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | """Codegen package responsive for generation of code for assembling. 2 | 3 | Provides different backends for OSxARCH pair 4 | """ 5 | 6 | from .generator import generate_code_for_assembler 7 | 8 | __all__ = ["generate_code_for_assembler"] 9 | -------------------------------------------------------------------------------- /tests/test_math_abs.gof: -------------------------------------------------------------------------------- 1 | /// Test absolute value function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 10 6 | 7 | func int main[] 8 | -5 abs 9 | 5 abs 10 | + return // |-5| + |5| => 10 11 | end -------------------------------------------------------------------------------- /libgofra/types/primitive/__init__.py: -------------------------------------------------------------------------------- 1 | """Primitive types for type system.""" 2 | 3 | from .boolean import BoolType 4 | from .character import CharType 5 | from .integers import I64Type 6 | from .void import VoidType 7 | 8 | __all__ = ("BoolType", "CharType", "I64Type", "VoidType") 9 | -------------------------------------------------------------------------------- /lib/memory.gof: -------------------------------------------------------------------------------- 1 | /// General helpers for memory usage 2 | 3 | // Set memory to int array from given pointer up to + SIZE with given int-value 4 | func void memset[*int[0] addr, int size, int value] 5 | for idx in 0..size do 6 | addr sizeof int idx * + value !< 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /libgofra/preprocessor/__init__.py: -------------------------------------------------------------------------------- 1 | """Preprocessor which resolves includes, CTE/macros and other from given lexer. 2 | 3 | Simply, wraps an lexer into another `lexer` and preprocess on the fly. 4 | """ 5 | 6 | from .preprocessor import preprocess_file 7 | 8 | __all__ = ("preprocess_file",) 9 | -------------------------------------------------------------------------------- /examples/01_hello_world.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // "Hello, World!" 3 | // Print an known string into console 4 | // ============================================== 5 | 6 | #include "std.gof" 7 | 8 | func void main[] 9 | "Hello, World!" println 10 | end -------------------------------------------------------------------------------- /tests/test_math_min_max.gof: -------------------------------------------------------------------------------- 1 | /// Test min value function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 13 6 | 7 | func int main[] 8 | 3 7 min 9 | 5 10 max 10 | + return // min(3, 7) + max(5, 10) => 3 + 10 => 13 11 | end -------------------------------------------------------------------------------- /libgofra/types/primitive/character.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import PrimitiveType 2 | 3 | 4 | class CharType(PrimitiveType): 5 | """Character type as an almost as-is alias to an integer.""" 6 | 7 | size_in_bytes = 1 8 | 9 | def __repr__(self) -> str: 10 | return "char" 11 | -------------------------------------------------------------------------------- /libgofra/types/primitive/integers.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import PrimitiveType 2 | 3 | 4 | class I64Type(PrimitiveType): 5 | """Integer of size 64 bits.""" 6 | 7 | size_in_bytes = 8 8 | 9 | def __repr__(self) -> str: 10 | return "I64" 11 | 12 | 13 | type AnyIntegerType = I64Type 14 | -------------------------------------------------------------------------------- /tests/test_array_global_single_element.gof: -------------------------------------------------------------------------------- 1 | /// Test that storing and loading from global static array returns same result 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 69 4 | 5 | var exit_code int[1] 6 | 7 | func int main[] 8 | &exit_code[0] TESTKIT_EXPECTED_EXIT_CODE !< 9 | 10 | exit_code[0] return 11 | end -------------------------------------------------------------------------------- /tests/test_array_local_single_element.gof: -------------------------------------------------------------------------------- 1 | /// Test that storing and loading from local stack array returns same result 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 69 4 | 5 | func int main[] 6 | var exit_code int[1] 7 | 8 | &exit_code[0] TESTKIT_EXPECTED_EXIT_CODE !< 9 | 10 | exit_code[0] return 11 | end -------------------------------------------------------------------------------- /tests/test_struct_static_initializer.gof: -------------------------------------------------------------------------------- 1 | /// Tests that structure initializer works for static allocated struct 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 32 4 | 5 | struct S 6 | A int 7 | end 8 | 9 | var x S = {A = TESTKIT_EXPECTED_EXIT_CODE}; 10 | 11 | func int main[] 12 | x.A return 13 | end 14 | 15 | -------------------------------------------------------------------------------- /gofra/execution/__init__.py: -------------------------------------------------------------------------------- 1 | """Execution helpers (e.g after compilation binary execution).""" 2 | 3 | from .execution import execute_binary_executable 4 | from .permissions import apply_file_executable_permissions 5 | 6 | __all__ = ( 7 | "apply_file_executable_permissions", 8 | "execute_binary_executable", 9 | ) 10 | -------------------------------------------------------------------------------- /scripts/run_acceptance_tests.sh: -------------------------------------------------------------------------------- 1 | # Perform all possible tests at user side, CI uses a bit different approach 2 | 3 | # Migrate to `set -xe` after fix with colors, TODO! 4 | set -e 5 | gofra-testkit -d examples --build-only -p "*_*.gof" -e 03_pong.gof -s 6 | gofra-testkit -d tests -s 7 | gofra ./examples/03_pong.gof -lraylib 8 | pytest . -------------------------------------------------------------------------------- /libgofra/types/primitive/floats.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import PrimitiveType 2 | 3 | 4 | class F64Type(PrimitiveType): 5 | """Float of size 64 bits.""" 6 | 7 | size_in_bytes = 8 8 | is_fp = True 9 | 10 | def __repr__(self) -> str: 11 | return "F64" 12 | 13 | 14 | type AnyFloatType = F64Type 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # By default assembler fresh create build cache directory contains .gitignore already 2 | # but we ensure that it is not included in the git VCS 3 | .build/ 4 | 5 | # Python 6 | *.pyc 7 | 8 | # MacOS 9 | .DS_STORE 10 | 11 | # Building artifacts 12 | dist 13 | site 14 | gofra/_distlib 15 | 16 | # Additional 17 | .VSCodeCounter -------------------------------------------------------------------------------- /tests/test_generics_identity_type.gof: -------------------------------------------------------------------------------- 1 | /// Test simple identity generic into concrete types 2 | type Identity{T} T 3 | 4 | func void main[] 5 | var int_identity Identity{T = int} = 5; 6 | var deep_pointer_identity Identity{T = **int}; 7 | 8 | 9 | &deep_pointer_identity &int_identity !< 10 | 11 | int_identity drop 12 | end -------------------------------------------------------------------------------- /tests/test_for_in_iterable_sum.gof: -------------------------------------------------------------------------------- 1 | /// Test that using `for-in` expression works 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 100 5 | 6 | var xs = [10, 20, 20, 50]; 7 | 8 | func int main[] 9 | var sum = 0; 10 | 11 | for x in xs do 12 | &sum x ?> sum + !< 13 | end 14 | 15 | sum return 16 | end 17 | -------------------------------------------------------------------------------- /libgofra/linker/output_format.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class LinkerOutputFormat(Enum): 5 | """Which type of output should base linker emit.""" 6 | 7 | # Simple executable 8 | EXECUTABLE = auto() 9 | 10 | # Library with symbols 11 | LIBRARY = auto() 12 | 13 | # Another object file 14 | OBJECT = auto() 15 | -------------------------------------------------------------------------------- /libgofra/types/primitive/boolean.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import PrimitiveType 2 | 3 | 4 | class BoolType(PrimitiveType): 5 | """Boolean type as an almost as-is alias to an integer.""" 6 | 7 | size_in_bytes = 8 # TODO(@kirillzhosul): Checkout usage without whole 8 bytes 8 | 9 | def __repr__(self) -> str: 10 | return "Bool" 11 | -------------------------------------------------------------------------------- /libgofra/optimizer/__init__.py: -------------------------------------------------------------------------------- 1 | """Optimizer package that used to apply different optimizations strategies for program.""" 2 | 3 | from .config import build_default_optimizer_config_from_level 4 | from .pipeline import create_optimizer_pipeline 5 | 6 | __all__ = ( 7 | "build_default_optimizer_config_from_level", 8 | "create_optimizer_pipeline", 9 | ) 10 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Gofra 2 | site_description: Documentation for Gofra programming language 3 | 4 | theme: 5 | name: mkdocs 6 | 7 | repo_url: https://github.com/kirillzhosul/gofra 8 | edit_uri: edit/main/docs/ 9 | 10 | nav: 11 | - About: index.md 12 | - Installation: installation.md 13 | - Tutorial: tutorial/index.md 14 | - Advanced: advanced/index.md -------------------------------------------------------------------------------- /tests/test_math_clamp.gof: -------------------------------------------------------------------------------- 1 | /// Test clamp value function from math library 2 | #include "os/io" 3 | #include "math.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 22 6 | 7 | func int main[] 8 | 15 5 10 clamp 9 | 3 5 10 clamp + 10 | 7 5 10 clamp + 11 | 12 | return // clamp(15, 5, 10) + clamp(3, 5, 10) + clamp(7, 5, 10) => 10 + 5 + 7 => 22 13 | end -------------------------------------------------------------------------------- /gofra/testkit/__main__.py: -------------------------------------------------------------------------------- 1 | """Python entry point for development environment run with `python -m gofra.testkit`. 2 | 3 | Should be exported as something like `gofra-testkit` system-wide. 4 | """ 5 | 6 | from .entry_point import cli_entry_point 7 | 8 | 9 | def main() -> None: 10 | cli_entry_point() 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Errors from typechecker for language user.""" 2 | 3 | from .missing_function_argument import MissingFunctionArgumentsTypecheckError 4 | from .parameter_type_mismatch import ParameterTypeMismatchTypecheckError 5 | 6 | __all__ = ( 7 | "MissingFunctionArgumentsTypecheckError", 8 | "ParameterTypeMismatchTypecheckError", 9 | ) 10 | -------------------------------------------------------------------------------- /libgofra/lexer/io/__init__.py: -------------------------------------------------------------------------------- 1 | """Input/Output (IO) helpers for filesystem within toolchain. 2 | 3 | It is an part of lexer due to toolchain built on top of lexer (as entry point to parser) and opening an file is responsibility of lexer. 4 | """ 5 | 6 | from .io import open_source_file, open_source_file_line_stream 7 | 8 | __all__ = ("open_source_file", "open_source_file_line_stream") 9 | -------------------------------------------------------------------------------- /tests/test_while_iterations.gof: -------------------------------------------------------------------------------- 1 | /// Test simple N iterations with while loop 2 | #include "os/general.gof" 3 | #include "assert.gof" 4 | 5 | const ITERATIONS = 10 6 | 7 | func void main[] 8 | var x = 0; 9 | 10 | while x ITERATIONS < do 11 | &x x 1 + !< 12 | end 13 | 14 | x ITERATIONS "While loop must perform exactly N iterations" call assert 15 | 16 | end -------------------------------------------------------------------------------- /tests/test_for_in_range_sum.gof: -------------------------------------------------------------------------------- 1 | /// Test that simple sum within `for in` loop works 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 5 4 | 5 | func int main[] 6 | var sum int = 0; 7 | 8 | for i in 0..15 do 9 | &sum sum 1 + !< 10 | end 11 | 12 | for i in 15..5 do // reverse for above but less on 5 13 | &sum sum 1 - !< 14 | end 15 | 16 | sum return 17 | end -------------------------------------------------------------------------------- /tests/test_std_random.gof: -------------------------------------------------------------------------------- 1 | /// Test random number generation from standard library 2 | #include "os/general.gof" 3 | #include "random.gof" 4 | #include "os/time.gof" 5 | 6 | #define TESTKIT_EXPECTED_EXIT_CODE 0 7 | 8 | no_return func void main[] 9 | time_seconds_since_epoch set_random_seed 10 | 11 | random drop 12 | 200 random_range drop 13 | 14 | 0 exit // TODO: Fix that 15 | end -------------------------------------------------------------------------------- /libgofra/linker/profile.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class LinkerProfile(Enum): 5 | """Profile for high-level API of Linker. 6 | 7 | Impacts optimizations and debug information. 8 | """ 9 | 10 | # Faster linkage, debug information is not touched 11 | DEBUG = auto() 12 | 13 | # May remove debug information and apply optimizations 14 | PRODUCTION = auto() 15 | -------------------------------------------------------------------------------- /libgofra/preprocessor/macros/defaults.py: -------------------------------------------------------------------------------- 1 | from libgofra.targets import Target 2 | 3 | 4 | def construct_propagated_toolchain_definitions( 5 | *, 6 | target: Target, 7 | ) -> dict[str, str]: 8 | return { 9 | f"ARCH_{target.architecture.upper()}": "1", 10 | f"OS_{target.operating_system.upper()}": "1", 11 | f"IS_{target.endianness.upper()}_ENDIAN": "1", 12 | } 13 | -------------------------------------------------------------------------------- /tests/test_sizeof_char_array.gof: -------------------------------------------------------------------------------- 1 | /// Test simple sizeof of char array (e.g mostly string), does not test for complex sizeof examples (e.g struct field sizeof, TODO) 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 32 5 | 6 | var buffer char[TESTKIT_EXPECTED_EXIT_CODE] 7 | 8 | func int main[] 9 | &buffer drop // silence warnings 10 | 11 | sizeof buffer return // todo 12 | end -------------------------------------------------------------------------------- /gofra/cache/vcs.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def create_cache_gitignore(path: Path) -> None: 5 | """Create .gitignore file for Git VCS to not include cache into VCS.""" 6 | with (path / ".gitignore").open("w") as f: 7 | f.write("# Internally created by Gofra toolchain\n") 8 | f.write("# Do not include this newly generated build cache into git VCS\n") 9 | f.write("*\n") 10 | -------------------------------------------------------------------------------- /docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced usage and tutorials 2 | 3 | 4 | ### [Preprocessor](./preprocessor.md) 5 | ### [FFI](./ffi.md) 6 | ### [Optimizations](./optimizations.md) 7 | ### [Type checker](./typechecker.md) 8 | ### [Toolchain Architecture](./toolchain-architecture.md) 9 | ### [Function Attributes](./function-attributes.md) 10 | ### [Type definition](./type-definition.md) 11 | ### [Generic Types](./generic-types.md) -------------------------------------------------------------------------------- /docs/tutorial/bitwise-operators.md: -------------------------------------------------------------------------------- 1 | # Bitwise operators 2 | 3 | # Shift left and right (<<, >>) 4 | 5 | Logical shift right and left 6 | Example 7 | ```gofra 8 | 2 3 << // logical shift 2 by 2 bits to the left 9 | 2 3 >> // logical shift 2 by 3 bits to the right 10 | ``` 11 | 12 | # AND (&) 13 | ```gofra 14 | 2 3 & // 0b10 & 0b11 -> 0b10 -> 2 15 | ``` 16 | 17 | # XOR (^) 18 | ```gofra 19 | 2 3 ^ // 0b10 ^0b11 -> 0b01 -> 1 20 | ``` -------------------------------------------------------------------------------- /lib/ffi.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // FFI (foreign-functions-interface) 3 | // ============================================== 4 | 5 | // TODO: Probably this may be inlined into compiler and language itself: https://github.com/kirillzhosul/gofra/issues/27 6 | 7 | // Convert an Gofra string to `C-String` via dropping size, suitable for FFI calls. 8 | func CStr to_cstr_ptr[*string sv] 9 | sv.data return 10 | end -------------------------------------------------------------------------------- /tests/test_abi_stack_spill_arguments.gof: -------------------------------------------------------------------------------- 1 | /// Test that arguments allowed to be spilled on stack (e.g lot of arguments) 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 25 4 | 5 | func int f[int r0, int r1, int r2, int r3, int r4, int r5, int r6, int r7, int r8, int r9, int r10, int r11, int r12, int r13, int r14, int r15] 6 | r10 r15 + return 7 | end 8 | 9 | func int main[] 10 | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 call f 11 | end 12 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Code generation backend module. 2 | 3 | Provides code generation backends (codegen) for emitting assembly from IR. 4 | """ 5 | 6 | from .aarch64.codegen import AARCH64CodegenBackend 7 | from .amd64.codegen import AMD64CodegenBackend 8 | from .base import CodeGeneratorBackend 9 | 10 | __all__ = [ 11 | "AARCH64CodegenBackend", 12 | "AMD64CodegenBackend", 13 | "CodeGeneratorBackend", 14 | ] 15 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/alignment.py: -------------------------------------------------------------------------------- 1 | def align_to_highest_size(size: int) -> int: 2 | """Shift given size so it aligns to 16 bytes (e.g 24 becomes 32). 3 | 4 | Modern architectures requires 16 bytes alignment, so that function will align that properly with some space left. 5 | """ 6 | assert size >= 0, "Cannot align negative or zero numbers" 7 | if size % 16 != 0: 8 | return (size + 15) & ~15 9 | return size 10 | -------------------------------------------------------------------------------- /libgofra/preprocessor/exceptions.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | 3 | 4 | class PreprocessorError(GofraError): 5 | """General error within preprocessor. 6 | 7 | Should not be used directly as not provides information about error source! 8 | """ 9 | 10 | def __repr__(self) -> str: 11 | return """General preprocessor error occurred. 12 | 13 | Please open an issue about that undocumented behavior! 14 | """ 15 | -------------------------------------------------------------------------------- /examples/02_input.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Simple input from user console 3 | // ============================================== 4 | 5 | #include "std.gof" 6 | 7 | var buf char[16] 8 | 9 | func void main[] 10 | var sv string; 11 | 12 | "Enter something: " print 13 | &buf sizeof buf input 14 | &sv.data &buf !< 15 | &sv.len sizeof buf !< 16 | 17 | "You entered: " print 18 | &sv println 19 | end -------------------------------------------------------------------------------- /tests/test_struct_simple_rw.gof: -------------------------------------------------------------------------------- 1 | /// Test that structs simply works by writing and reading value from its field 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 69 5 | 6 | struct struct_ 7 | _padding_left int 8 | field int // Padded from both sides to see if reading may fail 9 | _padding_right int 10 | end 11 | 12 | var x struct_ 13 | 14 | func int main[] 15 | &x.field TESTKIT_EXPECTED_EXIT_CODE !< 16 | 17 | x.field return 18 | end -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug: gofra (current args)", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "program": "-m", 9 | "args": [ 10 | "gofra", 11 | "${command:pickArgs}" 12 | ], 13 | "console": "integratedTerminal", 14 | "justMyCode": false 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tests/test_while_nested_counter.gof: -------------------------------------------------------------------------------- 1 | /// Test nested while loops counter 2 | 3 | #define A 3 4 | #define B 4 5 | 6 | #define TESTKIT_EXPECTED_EXIT_CODE 12 7 | 8 | func int main[] 9 | var counter = 0; 10 | var a = 0; 11 | var b = 0; 12 | 13 | while a A < do 14 | while b B < do 15 | &b b 1 + !< 16 | 17 | &counter counter 1 + !< 18 | end 19 | &b 0 !< 20 | &a a 1 + !< 21 | end 22 | 23 | counter return 24 | end -------------------------------------------------------------------------------- /docs/tutorial/index.md: -------------------------------------------------------------------------------- 1 | # Usage and tutorials 2 | 3 | ### [IDE and Editor support](../editors.md) 4 | ### [Control flow](./control-flow.md) 5 | ### [Memory management](./memory.md) 6 | ### [Stack management operators](./stack-management-operators.md) 7 | ### [Bitwise operators](./bitwise-operators.md) 8 | ### [Variables](./variables.md) 9 | ### [Structures](./structures.md) 10 | ### [Type casts](./typecasts.md) 11 | ### [Arrays](../datatypes/array.md) 12 | ### [Strings](../datatypes/string.md) 13 | -------------------------------------------------------------------------------- /tests/test_static_struct_passed_by_reference.gof: -------------------------------------------------------------------------------- 1 | /// Tests that passing structure as pointer to another function works and allows to work with that 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 69 5 | 6 | struct struct_ 7 | field int 8 | end 9 | 10 | var x struct_ 11 | 12 | func void f[*struct_ n] 13 | &n.field TESTKIT_EXPECTED_EXIT_CODE !< 14 | end 15 | 16 | no_return func void main[] 17 | &x call f 18 | 19 | x.field exit // TODO: Remove exit after fix 20 | end -------------------------------------------------------------------------------- /libgofra/types/_base.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable 2 | 3 | 4 | @runtime_checkable 5 | class Type(Protocol): 6 | """Base type for Gofra. 7 | 8 | Any type is children of that class, but they are splitted into 2 sub-types (Primitive and Composite type) 9 | """ 10 | 11 | size_in_bytes: int 12 | 13 | is_fp: bool = False 14 | 15 | 16 | # Base class for Primitive | Composite types 17 | class PrimitiveType(Type): ... 18 | 19 | 20 | class CompositeType(Type): ... 21 | -------------------------------------------------------------------------------- /tests/test_characters_as_integer.gof: -------------------------------------------------------------------------------- 1 | /// Test that characters correspond to their value as integer respectfully 2 | 3 | #include "assert.gof" 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 0 6 | 7 | func void main[] 8 | 'i' 105 "Characters must resolve to their char code (i)" assert 9 | '0' 48 "Characters must resolve to their char code (0)" assert 10 | '\n' 10 "Characters must resolve to their char code (\\n)" assert 11 | '\0' 0 "Characters must resolve to their char code (\\0)" assert 12 | end -------------------------------------------------------------------------------- /libgofra/hir/__init__.py: -------------------------------------------------------------------------------- 1 | """HIR - High Level Intermediate Representation. 2 | 3 | HIR is an form of representing code in level, higher from tokenizer lexemes (lexer tokens) 4 | For example `2 2 +` forms and [Operator.INTEGER(2), Operator.INTEGER(2), Operator.PLUS) 5 | (Complex examples is for example variable type definition which is an complex token stream formed in an container with another container with type) 6 | 7 | As Gofra is an concatenative stack-based language, HIR does not contain any AST (abstract-syntax-tree) 8 | """ 9 | -------------------------------------------------------------------------------- /libgofra/linker/apple/platforms.py: -------------------------------------------------------------------------------- 1 | # See `compose_apple_linker_command` `platform_version` 2 | from typing import Literal 3 | 4 | type APPLE_LINKER_PLATFORMS = Literal[ 5 | "macos", 6 | "ios", 7 | "tvos", 8 | "watchos", 9 | "bridgeos", 10 | "visionos", 11 | "xros", 12 | "mac-catalyst", 13 | "ios-simulator", 14 | "tvos-simulator", 15 | "watchos-simulator", 16 | "visionos-simulator", 17 | "xros-simulator", 18 | "driverkit", 19 | "firmware", 20 | "sepOS", 21 | ] 22 | -------------------------------------------------------------------------------- /docs/advanced/type-definition.md: -------------------------------------------------------------------------------- 1 | # Type definition 2 | 3 | Gofra has `type` keyword that allows define types (Generic types, type aliases) 4 | 5 | ## Type alias(ing) 6 | 7 | Type definition allows to define type alias to arbitrary data type 8 | ```gofra 9 | type Alias = int 10 | 11 | var x Alias = 5; // same as int 12 | ``` 13 | 14 | 15 | ## Generics 16 | 17 | Type definitions is also used to define an generic type 18 | ```gofra 19 | type Generic{T} = T 20 | ``` 21 | 22 | Read more at [Generic](./generic-types.md) types page 23 | -------------------------------------------------------------------------------- /tests/test_inline_asm_exit_darwin_arm.gof: -------------------------------------------------------------------------------- 1 | #ifdef ARCH_ARM64 #ifdef OS_DARWIN 2 | #define IS_SUITABLE_ENV 3 | #define TESTKIT_EXIT_CODE 2 4 | #endif #endif 5 | 6 | #ifndef IS_SUITABLE_ENV 7 | #define TESTKIT_EXIT_CODE 3 8 | #endif 9 | 10 | 11 | func int main[] 12 | #ifdef IS_SUITABLE_ENV 13 | inline_raw_asm "mov X16, #1" 14 | inline_raw_asm "mov X0, #2" 15 | inline_raw_asm "svc #0" 16 | #endif 17 | #ifndef IS_SUITABLE_ENV 18 | 3 return 19 | #endif 20 | 21 | 1 return 22 | end -------------------------------------------------------------------------------- /tests/test_numerical_literals.gof: -------------------------------------------------------------------------------- 1 | /// Test all numerical literals on stack 2 | 3 | #include "assert.gof" 4 | 5 | func int main[] 6 | 0 drop 7 | 128 drop 8 | 1024 drop 9 | 16777216 drop 10 | 11 | 0xFF 255 "Hex must equals to same value in 10-base " assert 12 | 0xFFFF 65535 "Hex must equals to same value in 10-base " assert 13 | 14 | // Max 16 bytes (64 bits) register 15 | // This is probably is 64bits arch related 16 | 18446744073709551615 drop 17 | 18 | 0 return 19 | end 20 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/sections/_alignment.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import Type 2 | 3 | 4 | def get_type_data_alignment(t: Type) -> int | None: 5 | """Get power-of-two alignment in bytes for specified type that will be located in data sections.""" 6 | # TODO(@kirillzhosul): Align by specifications of type not general byte size 7 | 8 | if t.size_in_bytes >= 16: 9 | return 4 10 | if t.size_in_bytes >= 8: 11 | return 3 12 | if t.size_in_bytes >= 4: 13 | return 2 14 | return None 15 | -------------------------------------------------------------------------------- /libgofra/types/primitive/void.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import PrimitiveType 2 | 3 | 4 | class VoidType(PrimitiveType): 5 | """Type that contains nothing, has no effect at runtime, rather compile-time and typechecker helper. 6 | 7 | E.g function that does not return any value has retval of type VOID, 8 | cogeneration can skip retval according to that as resolving that return value will mostly lead in stack corruption 9 | """ 10 | 11 | size_in_bytes = 0 12 | 13 | def __repr__(self) -> str: 14 | return "VOID" 15 | -------------------------------------------------------------------------------- /libgofra/preprocessor/macros/__init__.py: -------------------------------------------------------------------------------- 1 | """Preprocessor macros parser/resolver.""" 2 | 3 | from .macro import Macro 4 | from .preprocessor import ( 5 | consume_macro_definition_from_token, 6 | try_resolve_and_expand_macro_reference_from_token, 7 | ) 8 | from .registry import MacrosRegistry, registry_from_raw_definitions 9 | 10 | __all__ = ( 11 | "Macro", 12 | "MacrosRegistry", 13 | "consume_macro_definition_from_token", 14 | "registry_from_raw_definitions", 15 | "try_resolve_and_expand_macro_reference_from_token", 16 | ) 17 | -------------------------------------------------------------------------------- /tests/test_array_local_massive_blob.gof: -------------------------------------------------------------------------------- 1 | /// Test allocating an massive blob of memory at stack 2 | /// Currently this is capped due to local variables always stored locally 3 | /// This may be reworked after we introduce auto local variables as static 4 | 5 | #define TESTKIT_EXPECTED_EXIT_CODE 69 6 | 7 | // TODO: Add negative tests for that 8 | 9 | func int main[] 10 | var exit_code int[60] 11 | 12 | // That *massive* shift must be increased later. 13 | &exit_code[59] TESTKIT_EXPECTED_EXIT_CODE !< 14 | 15 | exit_code[59] return 16 | end -------------------------------------------------------------------------------- /libgofra/linker/gnu/target_formats.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | type GNU_LINKER_TARGET_FORMAT = Literal[ 4 | "elf64-x86-64", 5 | "elf32-i386", 6 | "elf32-iamcu", 7 | "elf32-x86-64", 8 | "elf64-little", 9 | "elf64-big", 10 | "elf32-little", 11 | "elf32-big", 12 | "pe-x86-64", 13 | "pei-x86-64", 14 | "pe-bigobj-x86-64", 15 | "pe-i386", 16 | "pei-i386", 17 | "pdb", 18 | "srec", 19 | "symbolsrec", 20 | "verilog", 21 | "tekhex", 22 | "binary", 23 | "ihex", 24 | "plugin", 25 | ] 26 | -------------------------------------------------------------------------------- /libgofra/parser/errors/general_expect_token.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import Token, TokenType 3 | 4 | 5 | class ExpectedTokenByParserError(GofraError): 6 | def __init__(self, *args: object, expected: TokenType, got: Token) -> None: 7 | super().__init__(*args) 8 | self.expected = expected 9 | self.got = got 10 | 11 | def __repr__(self) -> str: 12 | return f"""Expected {self.expected.name} but got {self.got.type.name} at {self.got.location}! 13 | 14 | {self.generic_error_name}""" 15 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/user_defined_compile_time_error.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class UserDefinedCompileTimeError(GofraError): 6 | def __init__(self, at: TokenLocation, message: str) -> None: 7 | self.at = at 8 | self.message = message 9 | 10 | def __repr__(self) -> str: 11 | return f"""Compilation error with message '{self.message}' at {self.at} 12 | 13 | This was raised from `compile_error` inside source code! 14 | 15 | {self.generic_error_name}""" 16 | -------------------------------------------------------------------------------- /tests/test_variable_default_integer_value.gof: -------------------------------------------------------------------------------- 1 | /// Test that assignment of default to stack/static variable works 2 | #include "std.gof" 3 | 4 | #define TESTKIT_EXPECTED_EXIT_CODE 30 5 | 6 | var static int = 20; 7 | 8 | func int test_local_stack_variable_initializer[] 9 | var stack int = 10; 10 | stack return 11 | end 12 | 13 | func int test_global_static_variable_initializer[] 14 | static return 15 | end 16 | 17 | func int main[] 18 | call test_global_static_variable_initializer 19 | call test_local_stack_variable_initializer 20 | + return 21 | end -------------------------------------------------------------------------------- /libgofra/lexer/errors/empty_character_literal.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class EmptyCharacterLiteralError(GofraError): 6 | def __init__(self, open_quote_at: TokenLocation) -> None: 7 | self.open_quote_at = open_quote_at 8 | 9 | def __repr__(self) -> str: 10 | return f"""Empty character literal at {self.open_quote_at}! 11 | 12 | Expected single *symbol* in character literal but got nothing! 13 | Did you forgot to enter character? 14 | 15 | {self.generic_error_name}""" 16 | -------------------------------------------------------------------------------- /libgofra/targets/infer_host.py: -------------------------------------------------------------------------------- 1 | from platform import system 2 | 3 | from libgofra.targets.target import Target 4 | 5 | 6 | def infer_host_target() -> Target | None: 7 | """Try to infer target from current system.""" 8 | match system(): 9 | case "Darwin": 10 | return Target.from_triplet("arm64-apple-darwin") 11 | case "Linux": 12 | return Target.from_triplet("amd64-unknown-linux") 13 | case "Windows": 14 | return Target.from_triplet("amd64-unknown-windows") 15 | case _: 16 | return None 17 | -------------------------------------------------------------------------------- /libgofra/exceptions.py: -------------------------------------------------------------------------------- 1 | import re 2 | from abc import abstractmethod 3 | 4 | 5 | def camel_to_kebab(s: str) -> str: 6 | return re.sub(r"(? str: 14 | return f"Some internal error occurred ({super().__repr__()}), that is currently not documented" 15 | 16 | @property 17 | def generic_error_name(self) -> str: 18 | return f"[{camel_to_kebab(self.__class__.__name__)}]" 19 | -------------------------------------------------------------------------------- /lib/os/os.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Operating system interactions wrapper 3 | // ============================================== 4 | 5 | // OS-dependant native abstraction layer 6 | #ifdef OS_WINDOWS #include "os/native/windows.gof" #endif 7 | #ifdef OS_DARWIN #include "os/native/darwin.gof" #endif 8 | #ifdef OS_LINUX #include "os/native/linux.gof" #endif 9 | 10 | #include "general.gof" 11 | 12 | // TODO: Probably would be cool and more user friendly if including something like `file` will include `file.gof` if there is no variants for module include -------------------------------------------------------------------------------- /libgofra/lexer/errors/unclosed_string_quote.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class UnclosedStringQuoteError(GofraError): 6 | def __init__(self, open_quote_at: TokenLocation) -> None: 7 | self.open_quote_at = open_quote_at 8 | 9 | def __repr__(self) -> str: 10 | return f"""Unclosed string quote at {self.open_quote_at}! 11 | 12 | Did you forgot to close string or mistyped string quote? 13 | Multi-line strings are prohibited, if this was your intention 14 | 15 | {self.generic_error_name}""" 16 | -------------------------------------------------------------------------------- /libgofra/lexer/__init__.py: -------------------------------------------------------------------------------- 1 | """Lexer package that used to lex source text into tokens (e.g tokenization). 2 | 3 | Lexer takes an input file and splits it content into tokens. 4 | 5 | Tokens and lexer implemented a bit odd (https://en.wikipedia.org/wiki/Lexical_analysis) 6 | Some token types are misleading, this code does not always imply conventions. 7 | """ 8 | 9 | from .keywords import Keyword 10 | from .lexer import tokenize_from_raw 11 | from .tokens import Token, TokenType 12 | 13 | __all__ = [ 14 | "Keyword", 15 | "Token", 16 | "TokenType", 17 | "tokenize_from_raw", 18 | ] 19 | -------------------------------------------------------------------------------- /lib/std.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Standard library 3 | // ============================================== 4 | 5 | // NOOP (NO OPeration) 6 | // Used to mark block without operation (as compiler will treat that as an error) 7 | // Compiler will optimize that with optimize level greater or 1 (`-O1`) 8 | #define noop 1 drop 9 | 10 | // Temporary backwards compatibility, quite volatile code and may be removed. 11 | #define inc 1 + 12 | #define dec 1 - 13 | 14 | #include "types.gof" 15 | #include "ffi.gof" 16 | 17 | #include "os" 18 | #include "os/io" 19 | 20 | #include "string.gof" -------------------------------------------------------------------------------- /libgofra/lexer/errors/unclosed_character_quote.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class UnclosedCharacterQuoteError(GofraError): 6 | def __init__(self, open_quote_at: TokenLocation) -> None: 7 | self.open_quote_at = open_quote_at 8 | 9 | def __repr__(self) -> str: 10 | return f"""Unclosed character quote at {self.open_quote_at}! 11 | 12 | Did you forgot to close character or mistyped character quote? 13 | Multi-line characters are prohibited, if this was your intention 14 | 15 | {self.generic_error_name}""" 16 | -------------------------------------------------------------------------------- /libgofra/parser/typecast.py: -------------------------------------------------------------------------------- 1 | from libgofra.hir.operator import OperatorType 2 | from libgofra.lexer.tokens import Token 3 | from libgofra.parser._context import ParserContext 4 | from libgofra.parser.type_parser import ( 5 | parse_concrete_type_from_tokenizer, 6 | ) 7 | 8 | 9 | def unpack_typecast_from_token(context: ParserContext, typecast_token: Token) -> None: 10 | typename = parse_concrete_type_from_tokenizer(context) 11 | 12 | context.push_new_operator( 13 | OperatorType.STATIC_TYPE_CAST, 14 | typecast_token, 15 | typename, 16 | is_contextual=False, 17 | ) 18 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/base.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import IO, Protocol 3 | 4 | from libgofra.hir.module import Module 5 | from libgofra.targets.target import Target 6 | 7 | 8 | class CodeGeneratorBackend(Protocol): 9 | """Base code generator backend protocol. 10 | 11 | All backends inherited from this protocol. 12 | """ 13 | 14 | def __init__( 15 | self, 16 | target: Target, 17 | module: Module, 18 | fd: IO[str], 19 | on_warning: Callable[[str], None], 20 | ) -> None: ... 21 | 22 | def emit(self) -> None: ... 23 | -------------------------------------------------------------------------------- /libgofra/parser/errors/top_level_expected_no_operators.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.hir.operator import Operator 3 | 4 | 5 | class TopLevelExpectedNoOperatorsError(GofraError): 6 | def __init__(self, operator: Operator) -> None: 7 | self.operator = operator 8 | 9 | def __repr__(self) -> str: 10 | return f"""Expected no operators at top-level 11 | 12 | Operators cannot appear at top-level, they must appear inside local scopes (e.g functions) 13 | First operator at top level was found here: {self.operator.location} 14 | 15 | {self.generic_error_name}""" 16 | -------------------------------------------------------------------------------- /lib/types.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Type definition and helpers 3 | // ============================================== 4 | 5 | type Array{T, N} T[N] 6 | type Pointer{T} *T 7 | 8 | type NullPtr *void 9 | type CStr *char[] 10 | 11 | // Boolean bindings for type-safety 12 | // Under optimizer this resolves into single boolean push onto stack 13 | // Probably, sometime this will be part of an language 14 | #define true 1 1 == 15 | #define false 1 0 == 16 | 17 | // Somewhere using terminology of NULL is more readable 18 | #define NULL 0 19 | #define NULL_PTR 0 typecast NullPtr 20 | -------------------------------------------------------------------------------- /tests/test_struct_field_type_forward_reference.gof: -------------------------------------------------------------------------------- 1 | /// Tests that forward referencing struct in itself is allowed 2 | 3 | #define TESTKIT_EXPECTED_EXIT_CODE 0 4 | 5 | struct forward_struct_t 6 | field forward_struct_t 7 | 8 | zero_padding char // TODO: This is required for forward references and MUST be considered as bug, as `forward_struct_t` has zero size during parsing this struct (also does not generate memory blob unless other fields specified) 9 | end 10 | 11 | var s forward_struct_t; 12 | 13 | func int main[] 14 | &s.field TESTKIT_EXPECTED_EXIT_CODE !< 15 | s.field typecast int return 16 | end 17 | -------------------------------------------------------------------------------- /libgofra/parser/errors/type_definition_already_exists.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class TypeDefinitionAlreadyExistsError(GofraError): 6 | def __init__(self, typename: str, redefined_at: TokenLocation) -> None: 7 | self.typename = typename 8 | self.redefined_at = redefined_at 9 | 10 | def __repr__(self) -> str: 11 | return f"""Type '{self.typename}' already defined! 12 | 13 | Tried to redefine type '{self.typename}' at {self.redefined_at} 14 | This type is already defined! 15 | 16 | {self.generic_error_name}""" 17 | -------------------------------------------------------------------------------- /libgofra/feature_flags.py: -------------------------------------------------------------------------------- 1 | """Some hardcoded feature flags that is not yet in the language but something like an proposal. 2 | 3 | They may break existing code or be buggy, so they are separate into flags 4 | You can enable them to try or to develop. 5 | """ 6 | 7 | # Allow to use float-values 8 | # FULLY UNSTABLE and has too few features 9 | # Merge plan: Full FP support like integers 10 | FEATURE_ALLOW_FPU = False 11 | 12 | # Will always generate OOB check with panic (abort) 13 | # on OOB within array access 14 | # Merge plan: add under flag, optimize usages of OOB check (not always) 15 | FEATURE_RUNTIME_ARRAY_OOB_CHECKS = False 16 | -------------------------------------------------------------------------------- /lib/os/io/std_handles.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Default handles / file descriptors 3 | // ============================================== 4 | 5 | #ifdef OS_WINDOWS 6 | #define STD_IN STD_INPUT_HANDLE 7 | #define STD_OUT STD_OUTPUT_HANDLE 8 | #define STD_ERR STD_ERROR_HANDLE 9 | #endif // OS_WINDOWS 10 | #ifdef OS_DARWIN 11 | #define STD_IN STD_IN_FD 12 | #define STD_OUT STD_OUT_FD 13 | #define STD_ERR STD_ERR_FD 14 | #endif // OS_DARWIN 15 | #ifdef OS_LINUX 16 | #define STD_IN STD_IN_FD 17 | #define STD_OUT STD_OUT_FD 18 | #define STD_ERR STD_ERR_FD 19 | #endif // OS_LINUX 20 | -------------------------------------------------------------------------------- /libgofra/codegen/get_backend.py: -------------------------------------------------------------------------------- 1 | from typing import assert_never 2 | 3 | from libgofra.targets import Target 4 | 5 | from .backends import ( 6 | AARCH64CodegenBackend, 7 | AMD64CodegenBackend, 8 | CodeGeneratorBackend, 9 | ) 10 | 11 | 12 | def get_backend_for_target( 13 | target: Target, 14 | ) -> type[CodeGeneratorBackend]: 15 | """Get code generator backend for specified ARCHxOS pair.""" 16 | match target.architecture: 17 | case "ARM64": 18 | return AARCH64CodegenBackend 19 | case "AMD64": 20 | return AMD64CodegenBackend 21 | case _: 22 | assert_never(target.architecture) 23 | -------------------------------------------------------------------------------- /libgofra/parser/errors/top_level_keyword_in_local_scope.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.keywords import Keyword 3 | from libgofra.lexer.tokens import Token 4 | 5 | 6 | class TopLevelKeywordInLocalScopeError(GofraError): 7 | def __init__(self, token: Token) -> None: 8 | self.token = token 9 | 10 | def __repr__(self) -> str: 11 | assert isinstance(self.token.value, Keyword) 12 | return f"""Top-level keyword used in local scope! 13 | 14 | Keyword '{self.token.value.name}' at {self.token.location} is root-only and cannot be used inside any local scope! 15 | 16 | {self.generic_error_name}""" 17 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/no_main_entry_function.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | 3 | 4 | class NoMainEntryFunctionError(GofraError): 5 | def __init__(self, expected_entry_name: str) -> None: 6 | self.expected_entry_name = expected_entry_name 7 | 8 | def __repr__(self) -> str: 9 | return f"""No entry point function `{self.expected_entry_name}` found! 10 | 11 | Typechecker expects executable entry point function symbol defined as `{self.expected_entry_name}` but it is missing 12 | Please define your entry function, or compile with output format without an entry point required 13 | 14 | {self.generic_error_name}""" 15 | -------------------------------------------------------------------------------- /libgofra/linker/apple/__init__.py: -------------------------------------------------------------------------------- 1 | """Apple Linker. 2 | 3 | Apple bundles MacOS (Darwin) with `ld` that is `apple-ld` 4 | This linker is only works with Mach-O format object files so it is only suitable for Darwin -> Darwin linkage 5 | 6 | It should be selected if you are on MacOS and compiling for Darwin target. 7 | I do not think it can be used outside of that workflow (even something like Linux -> Darwin should not be possible) 8 | 9 | According to that cross-compilation for Darwin targets is not possible, and must be done from Darwin (MacOS) host machine 10 | And by that, this linker uses native Darwin (and MacOS, like `xcrun`) possibilities and tools. 11 | """ 12 | -------------------------------------------------------------------------------- /libgofra/parser/errors/local_level_keyword_in_global_scope.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.keywords import Keyword 3 | from libgofra.lexer.tokens import Token 4 | 5 | 6 | class LocalLevelKeywordInGlobalScopeError(GofraError): 7 | def __init__(self, token: Token) -> None: 8 | self.token = token 9 | 10 | def __repr__(self) -> str: 11 | assert isinstance(self.token.value, Keyword) 12 | return f"""Local-level keyword used in global scope! 13 | 14 | Keyword '{self.token.value.name}' at {self.token.location} is local-only and cannot be used inside global scope! 15 | 16 | {self.generic_error_name}""" 17 | -------------------------------------------------------------------------------- /libgofra/parser/errors/unknown_primitive_type.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class UnknownPrimitiveTypeError(GofraError): 6 | def __init__(self, typename: str, at: TokenLocation) -> None: 7 | self.typename = typename 8 | self.at = at 9 | 10 | def __repr__(self) -> str: 11 | return f"""Unknown type '{self.typename}'! 12 | 13 | Expected known primitive type at {self.at} but got unknown '{self.typename}' 14 | Expected either an primitive type, aliased type, or structure name! 15 | (Or variable references in some expressions) 16 | 17 | {self.generic_error_name}""" 18 | -------------------------------------------------------------------------------- /libgofra/lexer/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Errors collections that lexer may raise (user-facing ones).""" 2 | 3 | from .ambiguous_hexadecimal_alphabet import AmbiguousHexadecimalAlphabetError 4 | from .empty_character_literal import EmptyCharacterLiteralError 5 | from .excessive_character_length import ExcessiveCharacterLengthError 6 | from .unclosed_character_quote import UnclosedCharacterQuoteError 7 | from .unclosed_string_quote import UnclosedStringQuoteError 8 | 9 | __all__ = [ 10 | "AmbiguousHexadecimalAlphabetError", 11 | "EmptyCharacterLiteralError", 12 | "ExcessiveCharacterLengthError", 13 | "UnclosedCharacterQuoteError", 14 | "UnclosedStringQuoteError", 15 | ] 16 | -------------------------------------------------------------------------------- /libgofra/parser/errors/keyword_in_without_loop_block.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.keywords import Keyword 3 | from libgofra.lexer.tokens import Token 4 | 5 | 6 | class KeywordInWithoutLoopBlockError(GofraError): 7 | def __init__(self, token: Token) -> None: 8 | self.token = token 9 | 10 | def __repr__(self) -> str: 11 | assert isinstance(self.token.value, Keyword) 12 | return f"""Keyword 'IN' used without context. 13 | 14 | Keyword '{Keyword.IN.name}' at {self.token.location} is used without context. 15 | This keyword must only appear in form of '{Keyword.FOR.name}'! 16 | 17 | {self.generic_error_name}""" 18 | -------------------------------------------------------------------------------- /tests/test_variables_rw.gof: -------------------------------------------------------------------------------- 1 | /// Test that RW for variables is working (both static and stack) 2 | #include "std.gof" 3 | #include "assert.gof" 4 | 5 | #define NUMBER 32 6 | var static int 7 | 8 | func void test_variables_rw_stack[] 9 | var stack int; 10 | 11 | &stack NUMBER !< 12 | 13 | stack NUMBER 14 | "Write and read on stack must return same value" assert 15 | end 16 | 17 | func void test_variables_rw_heap_static[] 18 | &static NUMBER !< 19 | 20 | static NUMBER 21 | "Write and read on heap must return same value" assert 22 | end 23 | 24 | func void main[] 25 | call test_variables_rw_stack 26 | call test_variables_rw_heap_static 27 | 28 | end -------------------------------------------------------------------------------- /libgofra/parser/errors/cannot_infer_var_type_from_initializer.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class CannotInferVariableTypeFromInitializerError(GofraError): 6 | def __init__(self, varname: str, at: TokenLocation) -> None: 7 | self.varname = varname 8 | self.at = at 9 | 10 | def __repr__(self) -> str: 11 | return f"""Unable to infer variable type from initializer! 12 | 13 | Variable '{self.varname}' defined at {self.at} has no type (auto type var) and unable to infer its type from specified initializer 14 | Consider adding type explicitly. 15 | 16 | {self.generic_error_name}""" 17 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/return_value_missing.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.hir.function import Function 3 | 4 | 5 | class ReturnValueMissingTypecheckError(GofraError): 6 | def __init__( 7 | self, 8 | owner: Function, 9 | ) -> None: 10 | super().__init__() 11 | self.owner = owner 12 | 13 | def __repr__(self) -> str: 14 | return f"""Return value missing 15 | 16 | Function '{self.owner.name}' defined at {self.owner.defined_at} 17 | return value type is '{self.owner.return_type}' but it is missing at the end (empty stack) 18 | 19 | Did you forgot to push return value? 20 | 21 | [return-value-missing]""" 22 | -------------------------------------------------------------------------------- /libgofra/types/__init__.py: -------------------------------------------------------------------------------- 1 | """Type system for Gofra. 2 | 3 | Does not contains any compile/run-time checks, only type definitions and system itself 4 | Type may be used both in high-level like typecheck system or in low-level one as code generation 5 | """ 6 | 7 | from ._base import CompositeType, PrimitiveType, Type 8 | from .composite import ArrayType, FunctionType, PointerType 9 | from .primitive import BoolType, CharType, I64Type, VoidType 10 | 11 | __all__ = [ 12 | "ArrayType", 13 | "BoolType", 14 | "CharType", 15 | "CompositeType", 16 | "FunctionType", 17 | "I64Type", 18 | "PointerType", 19 | "PrimitiveType", 20 | "Type", 21 | "VoidType", 22 | ] 23 | -------------------------------------------------------------------------------- /libgofra/lexer/errors/ambiguous_hexadecimal_alphabet.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class AmbiguousHexadecimalAlphabetError(GofraError): 6 | def __init__(self, at: TokenLocation, number_raw: str) -> None: 7 | self.number_raw = number_raw 8 | self.at = at 9 | 10 | def __repr__(self) -> str: 11 | # TODO(@kirillzhosul): Ambiguous location 12 | return f"""Ambiguous hex (16-base) alphabet at {self.at}! 13 | 14 | Invalid number: '{self.number_raw}' 15 | Ambiguous symbol: XXX TBD 16 | Hexadecimal numbers must consist only from symbols of hex alphabet (0-Z) 17 | 18 | {self.generic_error_name}""" 19 | -------------------------------------------------------------------------------- /examples/02_fib.gof: -------------------------------------------------------------------------------- 1 | 2 | // ============================================== 3 | // Fibonacci sequence 4 | // Print an series of Fibonacci numbers up to N 5 | // ============================================== 6 | 7 | #include "std.gof" 8 | 9 | const N = 4181; 10 | 11 | func void main[] 12 | var a = 0; 13 | var b = 1; 14 | var x int; 15 | &x a b + !< 16 | 17 | "Fibonacci Series: " print 18 | a print_int ", " print 19 | b print_int ", " print 20 | 21 | while x N <= do 22 | x print_int 23 | 24 | &a b !< 25 | &b x !< 26 | &x a b + !< 27 | 28 | x N <= if 29 | ", " print 30 | end 31 | end 32 | 33 | "\n" print 34 | end -------------------------------------------------------------------------------- /libgofra/parser/errors/unknown_field_accessor_struct_field.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | from libgofra.types.composite.structure import StructureType 4 | 5 | 6 | class UnknownFieldAccessorStructFieldError(GofraError): 7 | def __init__(self, field: str, at: TokenLocation, struct: StructureType) -> None: 8 | self.field = field 9 | self.at = at 10 | self.struct = struct 11 | 12 | def __repr__(self) -> str: 13 | return f"""Unknown field accessor! 14 | 15 | Trying to access field with name '{self.field} at {self.at} but it is not known within structure with name {self.struct.name} 16 | 17 | {self.generic_error_name}""" 18 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/entry_point_return_type_mismatch.py: -------------------------------------------------------------------------------- 1 | from libgofra.consts import GOFRA_ENTRY_POINT 2 | from libgofra.exceptions import GofraError 3 | from libgofra.types._base import Type 4 | 5 | 6 | class EntryPointReturnTypeMismatchTypecheckError(GofraError): 7 | def __init__(self, *args: object, return_type: Type) -> None: 8 | super().__init__(*args) 9 | self.return_type = return_type 10 | 11 | def __repr__(self) -> str: 12 | return f"""Entry point function '{GOFRA_ENTRY_POINT}' violates return type contract! 13 | 14 | Entry point function cannot have type contract out (e.g only `void` and `int` type is allowed)! 15 | But currently it has return type: {self.return_type} 16 | """ 17 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/entry_point_parameters_mismatch.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from libgofra.consts import GOFRA_ENTRY_POINT 4 | from libgofra.exceptions import GofraError 5 | from libgofra.types._base import Type 6 | 7 | 8 | class EntryPointParametersMismatchTypecheckError(GofraError): 9 | def __init__(self, *args: object, parameters: Sequence[Type]) -> None: 10 | super().__init__(*args) 11 | self.parameters = parameters 12 | 13 | def __repr__(self) -> str: 14 | return f"""Entry point function '{GOFRA_ENTRY_POINT}' violates parameters type contract! 15 | 16 | Entry point function cannot accept any parameters! 17 | But currently it has parameters: {self.parameters} 18 | """ 19 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/amd64/registers.py: -------------------------------------------------------------------------------- 1 | """Consts and types related to AMD64 registers and architecture (including FFI/ABI/IPC).""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Literal 6 | 7 | #### 8 | # Bare AMD64 related 9 | #### 10 | 11 | # Registers specification for AMD64 12 | # Skips some of registers due to currently being unused 13 | type AMD64_GP_REGISTERS = Literal[ 14 | "rax", 15 | "eax", 16 | "rbx", 17 | "rdi", 18 | "edx", 19 | "rsi", 20 | "rcx", 21 | "rdx", 22 | "r10", 23 | "r8", 24 | "r9", 25 | ] 26 | 27 | 28 | #### 29 | # Linux related 30 | #### 31 | 32 | # Epilogue 33 | AMD64_LINUX_EPILOGUE_EXIT_CODE = 0 34 | AMD64_LINUX_EPILOGUE_EXIT_SYSCALL_NUMBER = 60 35 | -------------------------------------------------------------------------------- /libgofra/parser/errors/constant_variable_requires_initializer.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class ConstantVariableRequiresInitializerError(GofraError): 6 | def __init__(self, varname: str, at: TokenLocation) -> None: 7 | self.varname = varname 8 | self.at = at 9 | 10 | def __repr__(self) -> str: 11 | return f"""Constant variable (definition) requires initial value (initializer)! 12 | 13 | Variable '{self.varname}' defined at {self.at} has no initial value / initializer 14 | Consider using direct variable (non-constant). 15 | 16 | Constants without initial value hame almost zero semantic sense. 17 | 18 | {self.generic_error_name}""" 19 | -------------------------------------------------------------------------------- /libgofra/preprocessor/include/distribution.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def infer_distribution_library_paths() -> list[Path]: 5 | """Infers paths to library distribution. 6 | 7 | (as package may be installed via package managers and they may mess with files includes). 8 | """ 9 | source_root = str(__import__("gofra").__file__) 10 | distribution_root = Path(source_root).parent 11 | assert distribution_root.exists(), ( 12 | "Corrupted distribution (dist parent is non-existent, unable to infer/resolve library paths)" 13 | ) 14 | return [ 15 | # default distribution 16 | distribution_root / "_distlib", 17 | # Local package 18 | distribution_root.parent / "lib", 19 | ] 20 | -------------------------------------------------------------------------------- /libgofra/optimizer/strategies/dead_code_elimination.py: -------------------------------------------------------------------------------- 1 | from libgofra.hir.module import Module 2 | from libgofra.optimizer.helpers.function_usage import search_unused_functions 3 | 4 | 5 | def optimize_dead_code_elimination( 6 | program: Module, 7 | max_iterations: int, 8 | ) -> None: 9 | """Perform DCE (dead-code-elimination) optimization onto program. 10 | 11 | Removes unused function from final program. 12 | TODO(@kirillzhosul): Does not optimize dead code. 13 | """ 14 | for _ in range(max_iterations): 15 | unused_functions = search_unused_functions(program) 16 | if not unused_functions: 17 | return 18 | 19 | for function in unused_functions: 20 | program.functions.pop(function.name) 21 | -------------------------------------------------------------------------------- /libgofra/lexer/errors/excessive_character_length.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class ExcessiveCharacterLengthError(GofraError): 6 | def __init__(self, open_quote_at: TokenLocation, excess_by_count: int) -> None: 7 | self.open_quote_at = open_quote_at 8 | self.excess_by_count = excess_by_count 9 | 10 | def __repr__(self) -> str: 11 | return f"""Excessive character literal length at {self.open_quote_at}! 12 | 13 | Expected only one *symbol* in character literal but got {self.excess_by_count}! 14 | 15 | Did you accidentally mix up character and string literal? 16 | Character literals are single symbol (unless escaping) 17 | 18 | {self.generic_error_name}""" 19 | -------------------------------------------------------------------------------- /editors/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gofra", 3 | "displayName": "Gofra", 4 | "version": "1.0.0", 5 | "engines": { 6 | "vscode": "^1.0.0" 7 | }, 8 | "contributes": { 9 | "languages": [ 10 | { 11 | "id": "gofra", 12 | "aliases": [ 13 | "Gofra" 14 | ], 15 | "extensions": [ 16 | ".gof", 17 | ".gofra" 18 | ] 19 | } 20 | ], 21 | "grammars": [ 22 | { 23 | "language": "gofra", 24 | "scopeName": "source.gofra", 25 | "path": "./syntaxes/gofra.tmLanguage.json" 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /libgofra/types/composite/function.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from libgofra.types._base import CompositeType, Type 4 | from libgofra.types.composite.pointer import PointerType 5 | 6 | 7 | class FunctionType(CompositeType): 8 | """Type that holds [pointer] to an function (e.g function itself as an type).""" 9 | 10 | parameter_types: Sequence[Type] 11 | return_type: Type 12 | 13 | size_in_bytes: int = PointerType.size_in_bytes 14 | 15 | def __init__(self, parameter_types: Sequence[Type], return_type: Type) -> None: 16 | self.parameter_types = parameter_types 17 | self.return_type = return_type 18 | 19 | def __repr__(self) -> str: 20 | return f"({', '.join(map(repr, self.parameter_types))}) -> {self.return_type}" 21 | -------------------------------------------------------------------------------- /libgofra/parser/errors/cannot_infer_var_type_from_empty_array_initializer.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | 4 | 5 | class CannotInferVariableTypeFromEmptyArrayInitializerError(GofraError): 6 | def __init__(self, varname: str, at: TokenLocation) -> None: 7 | self.varname = varname 8 | self.at = at 9 | 10 | def __repr__(self) -> str: 11 | return f"""Unable to infer variable type from initializer! 12 | 13 | Variable '{self.varname}' defined at {self.at} has no type (auto type var) and unable to infer its type from specified initializer 14 | Initializer has value of empty array which is impossible to infer types from 15 | Consider adding type explicitly. 16 | 17 | {self.generic_error_name}""" 18 | -------------------------------------------------------------------------------- /libgofra/parser/errors/variable_with_void_type.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import Token 3 | from libgofra.types.primitive.void import VoidType 4 | 5 | 6 | class VariableCannotHasVoidTypeParserError(GofraError): 7 | def __init__(self, varname: str, defined_at: Token) -> None: 8 | self.varname = varname 9 | self.defined_at = defined_at 10 | 11 | def __repr__(self) -> str: 12 | return f"""Void type is prohibited for variable types! 13 | 14 | Variable '{self.varname}' defined at {self.defined_at.location} has '{VoidType()}' type, which is prohibited! 15 | Using this type makes size of variables zero, which means it cannot have any value/invariant and has no meaning! 16 | 17 | [parser-variable-has-void-type]""" 18 | -------------------------------------------------------------------------------- /gofra/executable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | from gofra.cli.output import cli_message 7 | 8 | 9 | def cli_get_executable_program() -> str: 10 | """Get path to executable which is first argument (e.g `gofra ...`).""" 11 | return Path(sys.argv[0]).name 12 | 13 | 14 | def warn_on_improper_installation(executable: str) -> None: 15 | """Warn if user is calling CLI as Python module, e.g __main__.py.""" 16 | if not executable.endswith(".py"): 17 | return 18 | cli_message( 19 | level="WARNING", 20 | text=f"Running with prog == '{executable}', consider proper installation!", 21 | verbose=True, # Treat as always verbose - as this cannot be inferred from configuration sources. 22 | ) 23 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | """Build an distribution for publishing package to PyPi.""" 2 | 3 | from __future__ import annotations 4 | 5 | import subprocess 6 | from pathlib import Path 7 | from platform import system 8 | 9 | assert system() in ("Darwin", "Linux") 10 | 11 | ROOT = Path() / "../" 12 | LOCAL_LIBRARY_DIRECTORY = ROOT / "lib" 13 | DIST_LIBRARY_DIRECTORY = ROOT / "gofra" / "_distlib" 14 | 15 | 16 | def exec(*cmd: str | Path) -> None: # noqa: A001 17 | subprocess.run( 18 | list(map(str, cmd)), 19 | check=True, 20 | ) 21 | 22 | 23 | exec("cp", "-r", LOCAL_LIBRARY_DIRECTORY, DIST_LIBRARY_DIRECTORY) 24 | exec("poetry", "build") 25 | exec("find", DIST_LIBRARY_DIRECTORY, "-name", "*.gof", "-type", "f", "-delete") 26 | exec("find", DIST_LIBRARY_DIRECTORY, "-type", "d", "-delete") 27 | -------------------------------------------------------------------------------- /docs/tutorial/typecasts.md: -------------------------------------------------------------------------------- 1 | # Type casting 2 | 3 | There is always situations when you need to cast one type to another to silence errors from typechecker as it quite dumb and may stay on your way 4 | For that you have type casting feature (static type casts) only 5 | 6 | 7 | ## Static type cast 8 | 9 | Static type cast is an form of casting to an desired type which has no effect on runtime, only compile-time type checking only 10 | to statically cast to an type you may do following 11 | ```gofra 12 | var a char 13 | 14 | a typecast int 2 * // by default multiplying an char is not possible 15 | ``` 16 | 17 | typecasts is always in form of: 18 | ```gofra 19 | typecast {type_definition_from_scope} 20 | ``` 21 | 22 | You may write any arbitrary and composite / complex types inside typecast (e.g cast to an pointer to an struct) -------------------------------------------------------------------------------- /libgofra/types/composite/pointer.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | from libgofra.types._base import CompositeType, Type 4 | 5 | 6 | class PointerMemoryLocation(Enum): 7 | STACK = auto() 8 | HEAP = auto() 9 | STATIC = auto() 10 | 11 | 12 | class PointerType(CompositeType): 13 | """Type that holds another type it points to.""" 14 | 15 | points_to: Type 16 | size_in_bytes: int = 8 # Wide pointers as being on 64-bits machine 17 | 18 | memory_location: PointerMemoryLocation | None = None 19 | 20 | def __init__( 21 | self, 22 | points_to: Type, 23 | memory_location: PointerMemoryLocation | None = None, 24 | ) -> None: 25 | self.points_to = points_to 26 | self.memory_location = memory_location 27 | 28 | def __repr__(self) -> str: 29 | return f"*{self.points_to}" 30 | -------------------------------------------------------------------------------- /docs/datatypes/string.md: -------------------------------------------------------------------------------- 1 | # String data type 2 | 3 | Strings in Gofra are represented via *Fat Pointers* (Or also called *String View* and *String Slice*) 4 | 5 | Using `string` data type: 6 | ```gofra 7 | var str *string = "text" // Type may be omitted 8 | 9 | "another text" // Pushes *string on stack 10 | 11 | str.data // Push underlying *boxed* pointer to CStr (*char[]) 12 | str.len // Size (bytes) of the string (character length in UTF-8 as single byte for character) 13 | ``` 14 | 15 | Internally, `string` structure type is available for any compiled program and defined like so: 16 | ``` 17 | type struct string 18 | data *char[] 19 | len int 20 | end 21 | ``` 22 | 23 | That structure takes 16 bytes (8 bytes ptr, 8 bytes len) 24 | 25 | Strings defined internally in source code located in *static* data segment (must has Read-Only memory protection) -------------------------------------------------------------------------------- /lib/os/general.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // General operating system interactions 3 | // ============================================== 4 | 5 | // OS-dependant native abstraction layer 6 | #ifdef OS_WINDOWS #include "os/native/windows.gof" #endif 7 | #ifdef OS_DARWIN #include "os/native/darwin.gof" #endif 8 | #ifdef OS_LINUX #include "os/native/linux.gof" #endif 9 | 10 | // Exit an program with given exit code 11 | #ifdef OS_LINUX no_return func void exit[int] call sc_exit end #endif // TODO: must infer no return ? 12 | #ifdef OS_DARWIN no_return func void exit[int] call sc_exit end #endif // TODO: must infer no return ? 13 | #ifdef OS_WINDOWS no_return func void exit[int] call win_exit end #endif // TODO: must infer no return ? 14 | 15 | // Exit an program with error (1) exit code 16 | no_return inline func void abort[] 1 call exit end 17 | -------------------------------------------------------------------------------- /gofra/testkit/cli/matrix.py: -------------------------------------------------------------------------------- 1 | from gofra.cli.output import CLIColor 2 | from gofra.testkit.test import Test, TestStatus 3 | 4 | COLORS: dict[TestStatus, str] = { 5 | TestStatus.SUCCESS: CLIColor.GREEN, 6 | TestStatus.TOOLCHAIN_ERROR: CLIColor.RED, 7 | TestStatus.EXECUTION_STATUS_ERROR: CLIColor.RED, 8 | TestStatus.EXECUTION_TIMEOUT_ERROR: CLIColor.RED, 9 | TestStatus.SKIPPED: CLIColor.RESET, 10 | } 11 | 12 | ICONS: dict[TestStatus, str] = { 13 | TestStatus.SUCCESS: "+", 14 | TestStatus.TOOLCHAIN_ERROR: "-", 15 | TestStatus.EXECUTION_TIMEOUT_ERROR: "@", 16 | TestStatus.EXECUTION_STATUS_ERROR: "@", 17 | TestStatus.SKIPPED: ".", 18 | } 19 | 20 | 21 | def display_test_matrix(matrix: list[Test]) -> None: 22 | for test in matrix: 23 | color = COLORS[test.status] 24 | icon = ICONS[test.status] 25 | print(f"{color}{icon}", test.path) 26 | -------------------------------------------------------------------------------- /libgofra/types/registry.py: -------------------------------------------------------------------------------- 1 | from libgofra.types.composite.string import StringType 2 | from libgofra.types.generics import GenericParametrizedType 3 | from libgofra.types.primitive.boolean import BoolType 4 | from libgofra.types.primitive.character import CharType 5 | from libgofra.types.primitive.integers import I64Type 6 | from libgofra.types.primitive.void import VoidType 7 | 8 | from ._base import Type 9 | 10 | 11 | class TypeRegistry(dict[str, Type | GenericParametrizedType]): 12 | def copy(self) -> "TypeRegistry": 13 | return TypeRegistry(super().copy()) 14 | 15 | 16 | DEFAULT_PRIMITIVE_TYPE_REGISTRY = TypeRegistry( 17 | { 18 | "int": I64Type(), 19 | "i64": I64Type(), 20 | "char": CharType(), 21 | "void": VoidType(), 22 | "bool": BoolType(), 23 | # It is only half primitive 24 | "string": StringType(), 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/amd64/frame.py: -------------------------------------------------------------------------------- 1 | from libgofra.codegen.backends.alignment import align_to_highest_size 2 | from libgofra.codegen.backends.amd64._context import AMD64CodegenContext 3 | 4 | # Size of frame head (RBP register) 5 | FRAME_HEAD_SIZE = 8 6 | 7 | 8 | def preserve_calee_frame( 9 | context: AMD64CodegenContext, 10 | local_space_size: int, 11 | ) -> None: 12 | context.write("pushq %rbp") 13 | context.write("movq %rsp, %rbp") 14 | 15 | if local_space_size > 0: 16 | aligned_size = align_to_highest_size(local_space_size) 17 | context.write(f"subq ${aligned_size}, %rsp") 18 | 19 | 20 | def restore_calee_frame( 21 | context: AMD64CodegenContext, 22 | ) -> None: 23 | """Restore preserved by callee frame. 24 | 25 | Read more in: `preserve_callee_frame` 26 | """ 27 | context.write("movq %rbp, %rsp") 28 | context.write("popq %rbp") 29 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/sections/text_strings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from libgofra.codegen.sections._factory import SectionType 6 | 7 | if TYPE_CHECKING: 8 | from collections.abc import Mapping 9 | 10 | from libgofra.codegen.backends.aarch64._context import AARCH64CodegenContext 11 | 12 | 13 | def write_text_string_section( 14 | context: AARCH64CodegenContext, 15 | strings: Mapping[str, str], 16 | *, 17 | reference_suffix: str, 18 | ) -> None: 19 | """Emit text section with pure string text. 20 | 21 | :param reference_suffix: Append to each string symbol as it may overlap with real structure of string. 22 | """ 23 | if not strings: 24 | return 25 | context.section(SectionType.STRINGS) 26 | for name, data in strings.items(): 27 | context.fd.write(f'{name}{reference_suffix}: .asciz "{data}"\n') 28 | -------------------------------------------------------------------------------- /libgofra/linker/pkgconfig/pkgconfig.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from pathlib import Path 3 | from shutil import which 4 | from subprocess import DEVNULL, PIPE, run 5 | 6 | 7 | @lru_cache(maxsize=128) 8 | def pkgconfig_get_library_search_paths( 9 | library: str, 10 | ) -> list[Path] | None: 11 | """Get linker library search paths using `pkg-config` (e.g `--libs` yielding -l/-L flags) if it is installed in system.""" 12 | if not which("pkg-config"): 13 | # `pkg-config` is not installed / misconfigured in system - cannot use that 14 | return None 15 | 16 | command = ("pkg-config", "--libs", library) 17 | process = run( 18 | command, 19 | check=False, 20 | stdout=PIPE, 21 | stderr=DEVNULL, 22 | ) 23 | 24 | linker_flags = process.stdout.decode().split() 25 | return [ 26 | Path(flag.removeprefix("-L")) for flag in linker_flags if flag.startswith("-L") 27 | ] 28 | -------------------------------------------------------------------------------- /gofra/testkit/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum, auto 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from pathlib import Path 9 | from subprocess import CalledProcessError, TimeoutExpired 10 | 11 | from libgofra.exceptions import GofraError 12 | from libgofra.targets import Target 13 | 14 | 15 | class TestStatus(Enum): 16 | SKIPPED = auto() 17 | 18 | TOOLCHAIN_ERROR = auto() 19 | 20 | EXECUTION_STATUS_ERROR = auto() 21 | EXECUTION_TIMEOUT_ERROR = auto() 22 | 23 | SUCCESS = auto() 24 | 25 | 26 | type ERROR = GofraError | CalledProcessError | TimeoutExpired 27 | 28 | 29 | @dataclass(frozen=False) 30 | class Test: 31 | target: Target 32 | 33 | path: Path 34 | status: TestStatus 35 | 36 | expected_exit_code: int = 0 37 | 38 | error: ERROR | None = None 39 | 40 | artifact_path: Path | None = None 41 | -------------------------------------------------------------------------------- /libgofra/parser/errors/type_has_no_compile_time_initializer.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.lexer.tokens import TokenLocation 3 | from libgofra.types._base import Type 4 | 5 | 6 | class TypeHasNoCompileTimeInitializerParserError(GofraError): 7 | def __init__( 8 | self, 9 | type_with_no_initializer: Type, 10 | varname: str, 11 | at: TokenLocation, 12 | ) -> None: 13 | self.type_with_no_initializer = type_with_no_initializer 14 | self.at = at 15 | self.varname = varname 16 | 17 | def __repr__(self) -> str: 18 | return f"""No known initializer for type '{self.type_with_no_initializer}'! 19 | 20 | Variable '{self.varname}' defined at {self.at} has 21 | type '{self.type_with_no_initializer}' that has no initializer known at compile time! 22 | No known solution to initialize this variable. 23 | 24 | Consider using manual initializer logic. 25 | 26 | {self.generic_error_name}""" 27 | -------------------------------------------------------------------------------- /gofra/cli/goals/_optimization_pipeline.py: -------------------------------------------------------------------------------- 1 | from gofra.cli.output import cli_message 2 | from gofra.cli.parser.arguments import CLIArguments 3 | from libgofra.hir.module import Module 4 | from libgofra.optimizer.pipeline import create_optimizer_pipeline 5 | 6 | 7 | def cli_process_optimization_pipeline( 8 | program: Module, 9 | args: CLIArguments, 10 | ) -> None: 11 | """Apply optimization pipeline for program according to CLI arguments.""" 12 | cli_message( 13 | level="INFO", 14 | text=f"Applying optimizer pipeline (From base optimization level: {args.optimizer.level})", 15 | verbose=args.verbose, 16 | ) 17 | 18 | pipeline = create_optimizer_pipeline(args.optimizer) 19 | for optimizer_pass, optimizer_pass_name in pipeline: 20 | cli_message( 21 | level="INFO", 22 | text=f"Applying optimizer '{optimizer_pass_name}' pass", 23 | verbose=args.verbose, 24 | ) 25 | optimizer_pass(program) 26 | -------------------------------------------------------------------------------- /libgofra/codegen/generator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from pathlib import Path 3 | 4 | from libgofra.hir.module import Module 5 | from libgofra.targets import Target 6 | 7 | from .get_backend import get_backend_for_target 8 | 9 | 10 | def generate_code_for_assembler( 11 | output_path: Path, 12 | module: Module, 13 | target: Target, 14 | on_warning: Callable[[str], None], 15 | ) -> None: 16 | """Generate assembly from given program context and specified ARCHxOS pair into given file.""" 17 | backend_cls = get_backend_for_target(target) 18 | 19 | output_path.parent.mkdir(exist_ok=True) 20 | with output_path.open( 21 | mode="w", 22 | errors="strict", 23 | buffering=1, 24 | newline="", 25 | encoding="UTF-8", 26 | ) as fd: 27 | backend = backend_cls( 28 | fd=fd, 29 | target=target, 30 | module=module, 31 | on_warning=on_warning, 32 | ) 33 | return backend.emit() 34 | -------------------------------------------------------------------------------- /lib/os/time.gof: -------------------------------------------------------------------------------- 1 | 2 | // ============================================== 3 | // Time interaction wrapper 4 | // ============================================== 5 | 6 | // TODO: Add different implementation for Windows and non POSIX systems 7 | 8 | #include "general.gof" 9 | #include "types.gof" 10 | 11 | 12 | // Get current time of day as seconds from epoch 13 | func int time_seconds_since_epoch[] 14 | var tv timeval 15 | 16 | &tv NULL typecast *timezone sc_gettimeofday 17 | 18 | tv.tv_sec return 19 | end 20 | 21 | 22 | // Sleep for given amount of seconds 23 | func void sleep[int seconds] 24 | var tv timeval 25 | 26 | &tv.tv_sec seconds !< 27 | &tv.tv_usec 0 !< 28 | 29 | 0 NULL_PTR NULL_PTR NULL_PTR &tv call sc_select drop 30 | end 31 | 32 | // Sleep for given amount of microseconds 33 | func void sleep_ms[int ms] 34 | var tv timeval 35 | 36 | &tv.tv_sec ms 1000 / !< 37 | &tv.tv_usec ms 1000 % 1000 * !< 38 | 39 | 0 NULL_PTR NULL_PTR NULL_PTR &tv call sc_select drop 40 | end -------------------------------------------------------------------------------- /tests/test_boolean_logic_operators.gof: -------------------------------------------------------------------------------- 1 | /// Test that booleans are working and logic operators too 2 | 3 | #include "types.gof" 4 | #include "assert.gof" 5 | 6 | #define TESTKIT_EXPECTED_EXIT_CODE 0 7 | 8 | func int main[] 9 | true typecast int 10 | true typecast int 11 | "True must equals true" assert 12 | 13 | false typecast int 14 | false typecast int 15 | "False must equals false" assert 16 | 17 | true false && typecast int 18 | false typecast int 19 | "(true and false) must equal false" assert 20 | 21 | true false || typecast int 22 | true typecast int 23 | "(true or false) must equal true" assert 24 | 25 | false false || typecast int 26 | false typecast int 27 | "(false or false) must equal false" assert 28 | 29 | false false && typecast int 30 | false typecast int 31 | "(false and false) must equal false" assert 32 | 33 | TESTKIT_EXPECTED_EXIT_CODE return // TODO: Fix weird bug that requires this test to be an int-return 34 | end -------------------------------------------------------------------------------- /docs/advanced/toolchain-architecture.md: -------------------------------------------------------------------------------- 1 | # Toolchain (Compiler) architecture 2 | 3 | This page explains internals of compiler (toolchain) and is useful for development, continue reading if you need this 4 | 5 | ## Core modules 6 | 7 | Toolchain consists of widely used components: Lexer, Parser, Codegen but also contains Preprocessor, Optimizer, Typechecker and also relatable to notice is Linker, Assembler and CLI 8 | 9 | 10 | ## Toolchain CLI goal flow 11 | 12 | 13 | `CLI -> Lexer -> Preprocessor -> Parser -> Typechecker -> Optimizer -> Codegen -> Assembler -> Linker` 14 | 15 | 16 | ## Structure of default Code-generators 17 | 18 | By default all architectures emits assembly-code (e.g almost nearly machine code) that is requires to be assembler into binary machine code via Assembler 19 | 20 | 21 | ## Structure of typechecker 22 | 23 | As parser produce an HIR and does barely perform type checking, there is an Typechecker module which is responsible for checking that types are compatible within operations (e.g plus or function call) -------------------------------------------------------------------------------- /libgofra/parser/operators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from libgofra.hir.operator import OperatorType 4 | 5 | IDENTIFIER_TO_OPERATOR_TYPE = { 6 | "+": OperatorType.ARITHMETIC_PLUS, 7 | "-": OperatorType.ARITHMETIC_MINUS, 8 | "*": OperatorType.ARITHMETIC_MULTIPLY, 9 | "/": OperatorType.ARITHMETIC_DIVIDE, 10 | "%": OperatorType.ARITHMETIC_MODULUS, 11 | "<": OperatorType.COMPARE_LESS, 12 | ">": OperatorType.COMPARE_GREATER, 13 | "==": OperatorType.COMPARE_EQUALS, 14 | "!=": OperatorType.COMPARE_NOT_EQUALS, 15 | "<=": OperatorType.COMPARE_LESS_EQUALS, 16 | ">=": OperatorType.COMPARE_GREATER_EQUALS, 17 | "||": OperatorType.LOGICAL_OR, 18 | "&&": OperatorType.LOGICAL_AND, 19 | ">>": OperatorType.SHIFT_RIGHT, 20 | "<<": OperatorType.SHIFT_LEFT, 21 | "|": OperatorType.BITWISE_OR, 22 | "&": OperatorType.BITWISE_AND, 23 | "^": OperatorType.BITWISE_XOR, 24 | "?>": OperatorType.MEMORY_VARIABLE_READ, 25 | "!<": OperatorType.MEMORY_VARIABLE_WRITE, 26 | } 27 | -------------------------------------------------------------------------------- /lib/assert.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Runtime assertions 3 | // Check is value are equals without proper error handling 4 | // ============================================== 5 | 6 | #include "types.gof" 7 | #include "os/io/output.gof" 8 | 9 | // TODO: add static assertions (https://github.com/kirillzhosul/gofra/issues/19) 10 | 11 | // Check for equality at runtime with additional message 12 | // (assert two values are same) 13 | // TODO: add proper types to that function, as it may compare not only integers 14 | // probably this requires some overloading or something like that 15 | func void runtime_assert[int a, int b, *string sv] 16 | a b != if 17 | "runtime_assert: Assertion failed, " eprint 18 | sv call eprint 19 | "\n" call eprint 20 | call abort 21 | end 22 | end 23 | 24 | // *Export* simple `assert` as an alias to real `runtime_assert` 25 | // e.g static one will be `static_assert`, and this may be reworked, volatile code 26 | #define assert runtime_assert -------------------------------------------------------------------------------- /examples/02_cat.gof: -------------------------------------------------------------------------------- 1 | 2 | // ============================================== 3 | // `cat` analogue 4 | // Opens an text file from PATH macro and emits it content into standard 5 | // ============================================== 6 | 7 | #include "std.gof" 8 | 9 | // TODO(@kirillzhosul): Research why non-existent file returns STDERR FD 10 | // TODO: add argc argv 11 | // TODO: Windows support 12 | const PATH = "test.txt" 13 | 14 | // Some segment in memory so we can store content of the file 15 | // Global one as capacity is too big for stack (heap) 16 | var buf char[1024] 17 | 18 | 19 | func void main[] 20 | var sv string; 21 | var fd int; 22 | 23 | &sv.data &buf !< 24 | &sv.len sizeof buf !< 25 | 26 | &fd PATH call open_file_fd_ro !< 27 | fd 0 < if 28 | "Unable to open given file\n" eprint_fatal 29 | end 30 | 31 | // Read file 32 | 33 | fd &sv call read_file_sv false == if 34 | "Unable to read given file\n" eprint_fatal 35 | end 36 | 37 | // Print whole buffer 38 | &sv call print 39 | end -------------------------------------------------------------------------------- /lib/os/process.gof: -------------------------------------------------------------------------------- 1 | 2 | #include "os/io/output.gof" 3 | 4 | // Fork current process and return PID executed at two processes: Parent and Child 5 | // Aborts execution if fork failed (use underlying `sc_fork` as syscall to fork 6 | // TODO: Add Windows and OS guards 7 | func int fork[] 8 | var pid int; 9 | 10 | &pid call sc_fork !< // Fork current process and get an PID 11 | 12 | pid -1 == if 13 | // TODO: Not proper handling of error cases 14 | "Fork failed!" eprint_fatal 15 | end 16 | 17 | pid return 18 | end 19 | 20 | 21 | // TODO 22 | func void spawn[CStr, **char, **char] 23 | var pid int; 24 | &pid call fork !< // Fork current process and get an PID 25 | 26 | pid 0 == if 27 | // Child executes 28 | call sc_execve 29 | 30 | "'spawn' inside an child process did continued execution! Considering aborting child!" eprint_fatal 31 | drop return // todo 32 | end 33 | 34 | // Parent must still execute 35 | drop drop drop // todo 36 | end 37 | 38 | 39 | // TODO: Bare bone exec -------------------------------------------------------------------------------- /libgofra/parser/typedef.py: -------------------------------------------------------------------------------- 1 | from libgofra.lexer.tokens import TokenType 2 | from libgofra.parser._context import ParserContext 3 | from libgofra.parser.errors.type_definition_already_exists import ( 4 | TypeDefinitionAlreadyExistsError, 5 | ) 6 | from libgofra.parser.type_parser import ( 7 | consume_generic_type_parameters, 8 | parse_generic_type_alias_from_tokenizer, 9 | ) 10 | 11 | 12 | def unpack_type_definition_from_token(context: ParserContext) -> None: 13 | name_token = context.next_token() 14 | 15 | if name_token.type != TokenType.IDENTIFIER: 16 | msg = f"Expected type definition name at {name_token.location} to be an identifier but got {name_token.type.name}" 17 | raise ValueError(msg) 18 | 19 | typename = name_token.text 20 | if context.get_type(typename): 21 | raise TypeDefinitionAlreadyExistsError(typename, name_token.location) 22 | generic_type_params = consume_generic_type_parameters(context) 23 | context.types[typename] = parse_generic_type_alias_from_tokenizer( 24 | context, 25 | generic_type_params=generic_type_params, 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kirill Zhosul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/amd64/_context.py: -------------------------------------------------------------------------------- 1 | from collections.abc import MutableMapping 2 | from dataclasses import dataclass, field 3 | from typing import IO 4 | 5 | from libgofra.codegen.abi import AMD64ABI 6 | from libgofra.codegen.sections._factory import SectionType, get_os_assembler_section 7 | from libgofra.targets.target import Target 8 | 9 | 10 | @dataclass(frozen=True) 11 | class AMD64CodegenContext: 12 | """General context for emitting code from IR. 13 | 14 | Probably this is weird and bad way but OK for now. 15 | @kirillzhosul: Refactor at some point 16 | """ 17 | 18 | fd: IO[str] 19 | strings: MutableMapping[str, str] = field() 20 | target: Target 21 | 22 | abi: AMD64ABI 23 | 24 | def write(self, *lines: str) -> int: 25 | return self.fd.write("\t" + "\n\t".join(lines) + "\n") 26 | 27 | def section(self, section: SectionType) -> int: 28 | return self.fd.write( 29 | f".section {get_os_assembler_section(section, self.target)}\n", 30 | ) 31 | 32 | def load_string(self, string: str) -> str: 33 | string_key = "str%d" % len(self.strings) 34 | self.strings[string_key] = string 35 | return string_key 36 | -------------------------------------------------------------------------------- /libgofra/lexer/_state.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING, Literal 5 | 6 | from .tokens import TokenLocation 7 | 8 | if TYPE_CHECKING: 9 | from pathlib import Path 10 | 11 | 12 | @dataclass(frozen=False) 13 | class LexerState: 14 | """State for lexical analysis which only required for internal usages.""" 15 | 16 | path: Path | Literal["cli", "toolchain"] 17 | 18 | _row: int = 0 19 | col: int = 0 20 | 21 | _line: str = "" 22 | 23 | def current_location(self) -> TokenLocation: 24 | if self.path == "cli": 25 | return TokenLocation.cli() 26 | if self.path == "toolchain": 27 | return TokenLocation.toolchain() 28 | 29 | return TokenLocation( 30 | filepath=self.path, 31 | line_number=self.row, 32 | col_number=self.col, 33 | ) 34 | 35 | @property 36 | def row(self) -> int: 37 | return self._row 38 | 39 | @property 40 | def line(self) -> str: 41 | return self._line 42 | 43 | def set_line(self, row: int, line: str) -> None: 44 | self._row = row 45 | self._line = line 46 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/parameter_type_mismatch.py: -------------------------------------------------------------------------------- 1 | from libgofra.exceptions import GofraError 2 | from libgofra.hir.function import Function 3 | from libgofra.lexer.tokens import TokenLocation 4 | from libgofra.types import Type 5 | 6 | 7 | class ParameterTypeMismatchTypecheckError(GofraError): 8 | def __init__( 9 | self, 10 | expected_type: Type, 11 | actual_type: Type, 12 | callee: Function, 13 | caller: Function, 14 | at: TokenLocation, 15 | ) -> None: 16 | super().__init__() 17 | self.expected_type = expected_type 18 | self.actual_type = actual_type 19 | self.callee = callee 20 | self.caller = caller 21 | self.at = at 22 | 23 | def __repr__(self) -> str: 24 | return f"""Parameter type mismatch at {self.at} 25 | 26 | Function '{self.callee.name}{self.callee.parameters}' defined at {self.callee.defined_at} 27 | parameter type is '{self.expected_type}' but got argument of type '{self.actual_type}' 28 | 29 | Called from '{self.caller.name}' at {self.at} 30 | Mismatch: '{self.expected_type}' != '{self.actual_type}' 31 | 32 | Did you miss the types or missing typecast? 33 | [parameter-type-mismatch]""" 34 | -------------------------------------------------------------------------------- /libgofra/typecheck/entry_point.py: -------------------------------------------------------------------------------- 1 | from libgofra.hir.function import Function 2 | from libgofra.parser.exceptions import ParserEntryPointFunctionModifiersError 3 | from libgofra.typecheck.errors.entry_point_parameters_mismatch import ( 4 | EntryPointParametersMismatchTypecheckError, 5 | ) 6 | from libgofra.typecheck.errors.entry_point_return_type_mismatch import ( 7 | EntryPointReturnTypeMismatchTypecheckError, 8 | ) 9 | from libgofra.types.primitive.integers import I64Type 10 | 11 | 12 | def validate_entry_point_signature(entry_point: Function) -> None: 13 | # TODO(@kirillzhosul): these parser errors comes from legacy entry point validation, must be reworked later - https://github.com/kirillzhosul/gofra/issues/28 14 | 15 | if entry_point.is_external or entry_point.is_inline: 16 | raise ParserEntryPointFunctionModifiersError 17 | 18 | retval_t = entry_point.return_type 19 | if entry_point.has_return_value() and not isinstance(retval_t, I64Type): 20 | raise EntryPointReturnTypeMismatchTypecheckError(return_type=retval_t) 21 | 22 | if entry_point.parameters: 23 | raise EntryPointParametersMismatchTypecheckError( 24 | parameters=entry_point.parameters, 25 | ) 26 | -------------------------------------------------------------------------------- /libgofra/linker/apple/architectures.py: -------------------------------------------------------------------------------- 1 | # Architectures that is supported by Apple Linker 2 | # may be different on some machines with MacOS 3 | from collections.abc import Mapping 4 | from typing import Literal 5 | 6 | from libgofra.targets.target import Target 7 | 8 | type APPLE_LINKER_ARCHITECTURES = Literal[ 9 | "armv6", 10 | "armv7", 11 | "armv7s", 12 | "arm64", 13 | "arm64e", 14 | "arm64_32", 15 | "i386", 16 | "x86_64", 17 | "x86_64h", 18 | "armv6m", 19 | "armv7k", 20 | "armv7m", 21 | "armv7em", 22 | "armv8m.main", 23 | "armv8.1m.main", 24 | ] 25 | 26 | 27 | # Mapping for `apple_linker_architecture_from_target` 28 | APPLE_LINKER_TARGET_ARCHITECTURE_MAPPING: Mapping[ 29 | Literal["ARM64", "AMD64"], 30 | Literal["arm64", "x86_64"], 31 | ] = { 32 | "ARM64": "arm64", 33 | "AMD64": "x86_64", 34 | } 35 | 36 | 37 | def apple_linker_architecture_from_target(target: Target) -> APPLE_LINKER_ARCHITECTURES: 38 | assert target.architecture in APPLE_LINKER_TARGET_ARCHITECTURE_MAPPING, ( 39 | "Unknown target architecture for Apple linker. This is an bug in Gofra toolchain!" 40 | ) 41 | return APPLE_LINKER_TARGET_ARCHITECTURE_MAPPING[target.architecture] 42 | -------------------------------------------------------------------------------- /libgofra/typecheck/errors/missing_function_argument.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from libgofra.exceptions import GofraError 4 | from libgofra.hir.function import Function 5 | from libgofra.lexer.tokens import TokenLocation 6 | from libgofra.types import Type 7 | 8 | 9 | class MissingFunctionArgumentsTypecheckError(GofraError): 10 | def __init__( 11 | self, 12 | *args: object, 13 | typestack: Sequence[Type], 14 | caller: Function, 15 | callee: Function, 16 | at: TokenLocation, 17 | ) -> None: 18 | super().__init__(*args) 19 | self.typestack = typestack 20 | self.callee = callee 21 | self.caller = caller 22 | self.at = at 23 | 24 | def __repr__(self) -> str: 25 | missing_count = len(self.callee.parameters) - len(self.typestack) 26 | return f"""Not enough function arguments at {self.at} 27 | 28 | Function '{self.callee.name}{self.callee.parameters}' defined at {self.callee.defined_at} 29 | expects {missing_count} more arguments on stack 30 | 31 | Mismatch (stack): {self.typestack} != {self.callee.parameters} 32 | 33 | Called from '{self.caller.name}' at {self.at} 34 | 35 | [missing-function-arguments]""" 36 | -------------------------------------------------------------------------------- /gofra/cli/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from gofra.cli.errors.error_handler import cli_gofra_error_handler 4 | from gofra.cli.goals import perform_desired_toolchain_goal 5 | from gofra.cli.parser.builder import build_cli_parser 6 | from gofra.cli.parser.parser import parse_cli_arguments 7 | from gofra.executable import cli_get_executable_program, warn_on_improper_installation 8 | 9 | from .output import cli_fatal_abort 10 | 11 | 12 | def cli_entry_point() -> None: 13 | """CLI main entry.""" 14 | prog = cli_get_executable_program() 15 | warn_on_improper_installation(prog) 16 | 17 | parser = build_cli_parser(prog) 18 | args = parse_cli_arguments(parser.parse_args()) 19 | wrapper = cli_gofra_error_handler( 20 | debug_user_friendly_errors=args.cli_debug_user_friendly_errors, 21 | ) 22 | 23 | with wrapper: 24 | # Wrap goal into error handler as in unwraps errors into user-friendly ones (except internal ones as bugs) 25 | perform_desired_toolchain_goal(args) 26 | 27 | # This is unreachable but error wrapper must fail 28 | cli_fatal_abort("Bug in a CLI: toolchain must perform at least one goal!") 29 | 30 | 31 | if __name__ == "__main__": 32 | cli_entry_point() 33 | -------------------------------------------------------------------------------- /libgofra/optimizer/helpers/function_usage.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from itertools import chain 3 | 4 | from libgofra.hir.function import Function 5 | from libgofra.hir.module import Module 6 | from libgofra.parser.operators import OperatorType 7 | 8 | 9 | def is_function_has_callers(program: Module, function_name: str) -> bool: 10 | """Check is given function was called at least once in whole program.""" 11 | assert function_name in program.functions, ( 12 | "Expected existing function in `is_function_has_callers`" 13 | ) 14 | 15 | function = program.functions[function_name] 16 | 17 | return any( 18 | operator.type == OperatorType.FUNCTION_CALL 19 | and operator.operand == function.name 20 | for operator in chain.from_iterable( 21 | (f.operators for f in program.functions.values() if not f.is_leaf), 22 | ) 23 | ) 24 | 25 | 26 | def search_unused_functions(program: Module) -> Iterable[Function]: 27 | """Search unused functions without any usage (excluding global function symbols).""" 28 | return [ 29 | f 30 | for f in program.functions.values() 31 | if not f.is_global and not is_function_has_callers(program, f.name) 32 | ] 33 | -------------------------------------------------------------------------------- /libgofra/preprocessor/conditions/exceptions.py: -------------------------------------------------------------------------------- 1 | from libgofra.lexer.tokens import Token 2 | from libgofra.preprocessor.exceptions import PreprocessorError 3 | 4 | 5 | class PreprocessorConditionalNoMacroNameError(PreprocessorError): 6 | def __init__(self, *args: object, conditional_token: Token) -> None: 7 | super().__init__(*args) 8 | self.conditional_token = conditional_token 9 | 10 | def __repr__(self) -> str: 11 | return f"""No macro name specified at preprocessor condition at {self.conditional_token.location} 12 | 13 | Expected macro name after conditional block!""" 14 | 15 | 16 | class PreprocessorConditionalConsumeUntilEndifContextSwitchError(PreprocessorError): 17 | def __init__(self, *args: object, conditional_token: Token) -> None: 18 | super().__init__(*args) 19 | self.conditional_token = conditional_token 20 | 21 | def __repr__(self) -> str: 22 | return f"""Preprocessor conditional block got context (file) switch while consuming an block at {self.conditional_token.location}. 23 | 24 | This is caused probably by an unclosed preprocessor block (e.g no `#endif`). 25 | (Consuming an conditional block should not switch an tokenizer context, as is means current processed file is probably changed).""" 26 | -------------------------------------------------------------------------------- /gofra/cli/output.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from sys import stderr, stdout 3 | from typing import Literal, NoReturn 4 | 5 | type MessageLevel = Literal["INFO", "ERROR", "WARNING", "SUCCESS"] 6 | 7 | 8 | class CLIColor: 9 | RED = "\033[91m" 10 | GREEN = "\033[92m" 11 | RESET = "\033[0m" 12 | YELLOW = "\033[93m" 13 | 14 | 15 | def cli_message(level: MessageLevel, text: str, *, verbose: bool = True) -> None: 16 | """Emit an message to CLI user with given level, applying FD according to level.""" 17 | fd = stdout if level not in ("ERROR",) else stderr 18 | 19 | if level == "INFO" and not verbose: 20 | return 21 | 22 | color_mapping: dict[MessageLevel, str] = { 23 | "ERROR": CLIColor.RED, 24 | "WARNING": CLIColor.YELLOW, 25 | "SUCCESS": CLIColor.GREEN, 26 | } 27 | print( 28 | f"{color_mapping.get(level, CLIColor.RESET)}[{level}] {text}{CLIColor.RESET}", 29 | file=fd, 30 | ) 31 | 32 | 33 | def cli_fatal_abort(text: str) -> NoReturn: 34 | cli_message(level="ERROR", text=text, verbose=True) 35 | sys.exit(1) 36 | 37 | 38 | def cli_linter_warning(text: str, *, verbose: bool = True) -> None: 39 | """Wrap `cli_message` for easy grep-ing for warnings emitted for user.""" 40 | return cli_message("WARNING", text, verbose=verbose) 41 | -------------------------------------------------------------------------------- /libgofra/types/composite/array.py: -------------------------------------------------------------------------------- 1 | from libgofra.types._base import CompositeType, Type 2 | 3 | 4 | class ArrayType(CompositeType): 5 | """Contiguous blob of memory that contains specified type in each section of size of that element.""" 6 | 7 | _element_type: Type 8 | _elements_count: int 9 | 10 | size_in_bytes: int 11 | 12 | def __init__(self, element_type: Type, elements_count: int) -> None: 13 | assert elements_count >= 0 14 | self._element_type = element_type 15 | self.elements_count = elements_count 16 | 17 | def get_index_offset(self, index: int) -> int: 18 | """Calculate offset from start of an array to element with given index.""" 19 | return self.element_type.size_in_bytes * index 20 | 21 | def is_index_oob(self, index: int) -> int: 22 | return index >= self.elements_count 23 | 24 | def __repr__(self) -> str: 25 | return f"{self.element_type}[{self.elements_count}]" 26 | 27 | @property 28 | def elements_count(self) -> int: 29 | return self._elements_count 30 | 31 | @property 32 | def element_type(self) -> Type: 33 | return self._element_type 34 | 35 | @elements_count.setter 36 | def elements_count(self, value: int) -> None: 37 | self._elements_count = value 38 | self.size_in_bytes = self.element_type.size_in_bytes * self.elements_count 39 | -------------------------------------------------------------------------------- /gofra/cli/errors/error_handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections.abc import Generator 3 | from contextlib import contextmanager 4 | from subprocess import CalledProcessError 5 | from typing import NoReturn 6 | 7 | from gofra.cli.output import cli_fatal_abort, cli_message 8 | from libgofra.exceptions import GofraError 9 | 10 | 11 | @contextmanager 12 | def cli_gofra_error_handler( 13 | *, 14 | debug_user_friendly_errors: bool = True, 15 | ) -> Generator[None, None, NoReturn]: 16 | """Wrap function to properly emit Gofra internal errors.""" 17 | try: 18 | yield 19 | except GofraError as ge: 20 | if debug_user_friendly_errors: 21 | return cli_fatal_abort(repr(ge)) 22 | raise # re-throw exception due to unfriendly flag set for debugging 23 | except CalledProcessError as pe: 24 | command = " ".join(pe.cmd) 25 | cli_message( 26 | "ERROR", 27 | f"""Command process with cmd: {command} failed with exit code {pe.returncode}""", 28 | ) 29 | # Propagate exit code from called process 30 | return sys.exit(pe.returncode) 31 | except KeyboardInterrupt: 32 | print() 33 | cli_message("INFO", "Interrupted by user (Ctrl+C)!") 34 | return sys.exit(0) 35 | # This is unreachable but error wrapper must fail 36 | cli_fatal_abort("Bug in a CLI: error handler must has no-return") 37 | -------------------------------------------------------------------------------- /libgofra/assembler/assembler.py: -------------------------------------------------------------------------------- 1 | """Assembler module to assemble programs in Gofra language into executables.""" 2 | 3 | from __future__ import annotations 4 | 5 | from subprocess import CompletedProcess, run 6 | from typing import TYPE_CHECKING 7 | 8 | from gofra.cli.output import cli_fatal_abort 9 | 10 | from .clang import clang_driver_is_installed, compose_clang_assembler_command 11 | 12 | if TYPE_CHECKING: 13 | from pathlib import Path 14 | 15 | from libgofra.targets import Target 16 | 17 | 18 | def assemble_object_from_codegen_assembly( 19 | assembly: Path, 20 | output: Path, 21 | target: Target, 22 | *, 23 | debug_information: bool, 24 | additional_assembler_flags: list[str], 25 | ) -> CompletedProcess[bytes]: 26 | """Convert given program into object using assembler for next linkage step.""" 27 | if not clang_driver_is_installed(): 28 | cli_fatal_abort("Clang driver is not installed, cannot assemble, aborting!") 29 | 30 | clang_command = compose_clang_assembler_command( 31 | assembly, 32 | output, 33 | target=target, 34 | debug_information=debug_information, 35 | additional_assembler_flags=additional_assembler_flags, 36 | ) 37 | process = run( 38 | clang_command, 39 | check=False, 40 | capture_output=False, 41 | ) 42 | process.check_returncode() 43 | return process 44 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/_context.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, MutableMapping 2 | from dataclasses import dataclass, field 3 | from typing import IO 4 | 5 | from libgofra.codegen.abi import AARCH64ABI 6 | from libgofra.codegen.sections._factory import SectionType, get_os_assembler_section 7 | from libgofra.targets.target import Target 8 | 9 | 10 | @dataclass(frozen=True) 11 | class AARCH64CodegenContext: 12 | """General context for emitting code from IR. 13 | 14 | Probably this is weird and bad way but OK for now. 15 | @kirillzhosul: Refactor at some point 16 | """ 17 | 18 | on_warning: Callable[[str], None] 19 | fd: IO[str] 20 | abi: AARCH64ABI 21 | target: Target 22 | strings: MutableMapping[str, str] = field( 23 | default_factory=dict[str, str], 24 | ) 25 | 26 | def write(self, *lines: str) -> int: 27 | return self.fd.write("\t" + "\n\t".join(lines) + "\n") 28 | 29 | def section(self, section: SectionType) -> int: 30 | return self.fd.write( 31 | f".section {get_os_assembler_section(section, self.target)}\n", 32 | ) 33 | 34 | def comment(self, line: str) -> int: 35 | return self.write(f"// {line}") 36 | 37 | def load_string(self, string: str) -> str: 38 | string_key = ".str%d" % len(self.strings) 39 | self.strings[string_key] = string 40 | return string_key 41 | -------------------------------------------------------------------------------- /libgofra/lexer/io/exceptions.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from libgofra.exceptions import GofraError 4 | 5 | 6 | class IOFileDoesNotExistsError(GofraError): 7 | def __init__(self, *args: object, path: Path) -> None: 8 | super().__init__(*args) 9 | self.path = path 10 | 11 | def __repr__(self) -> str: 12 | return f"""File not found (I/O error) 13 | 14 | Tried to open file at path: `{self.path}` 15 | (Resolves to: `{self.path.resolve()}) 16 | 17 | Please ensure that requested file/symlink exists!""" 18 | 19 | 20 | class IOFileNotAnFileError(GofraError): 21 | def __init__(self, *args: object, path: Path) -> None: 22 | super().__init__(*args) 23 | self.path = path 24 | 25 | def __repr__(self) -> str: 26 | return f"""File is not an file 27 | (Is an directory?: {"Yes" if self.path.is_dir() else "No"}) 28 | 29 | Tried to open file at path: `{self.path}` 30 | (Resolves to: `{self.path.resolve()}) 31 | 32 | Please ensure that requested file is an file and not an directory or anything else!""" 33 | 34 | 35 | class IOFileNotAnTextFileError(GofraError): 36 | def __init__(self, *args: object, path: Path) -> None: 37 | super().__init__(*args) 38 | self.path = path 39 | 40 | def __repr__(self) -> str: 41 | return f"""File is not an text file (contains non UTF-8 text) 42 | 43 | Tried to read file at path: `{self.path}`""" 44 | -------------------------------------------------------------------------------- /gofra/cache/directory.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from gofra.cache.vcs import create_cache_gitignore 4 | 5 | INCLUDED_CLEANUP_EXTENSIONS = {"", ".s", ".o"} 6 | EXCLUDED_CLEANUP_FILENAMES = {".gitignore"} 7 | 8 | 9 | def prepare_build_cache_directory(path: Path) -> None: 10 | """Try to create and fill cache directory with required files.""" 11 | if path.exists(): 12 | return 13 | 14 | path.mkdir(exist_ok=False) 15 | create_cache_gitignore(path) 16 | 17 | 18 | def cleanup_build_cache_directory(path: Path) -> None: 19 | """Remove all files in cache directory that is related to Gofra. 20 | 21 | (related unless user places any same files in cache directory). 22 | """ 23 | # TODO(@kirillzhosul): Cleanup of cache directory is not implemented yet. 24 | for file in path.iterdir(): 25 | if not file.is_file(): 26 | continue 27 | if not _is_cache_file_removable(path, file): 28 | return 29 | file.unlink(missing_ok=True) 30 | 31 | 32 | def _is_cache_file_removable(cache_path: Path, path: Path) -> bool: 33 | """Check is given file related to cache directory and can be safely removed in any way.""" 34 | return ( 35 | path.absolute().is_relative_to(cache_path.absolute()) # Must be children 36 | and path.suffix in INCLUDED_CLEANUP_EXTENSIONS 37 | and path.name not in EXCLUDED_CLEANUP_FILENAMES 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_array_character_word.gof: -------------------------------------------------------------------------------- 1 | /// Test that storing an word in array as characters and printing it shows that word 2 | 3 | #include "os/io/output.gof" 4 | #include "os/general.gof" 5 | #define TESTKIT_EXPECTED_EXIT_CODE 0 6 | #define TESTKIT_EXPECTED_STDOUT "Global\nLocal\n" 7 | 8 | var global_ char[7]; // TODO: `global` is treated as keyword 9 | 10 | func void test_array_global_character_word[] 11 | var sv string; 12 | 13 | &global_[0] 'G' !< 14 | &global_[1] 'l' !< 15 | &global_[2] 'o' !< 16 | &global_[3] 'b' !< 17 | &global_[4] 'a' !< 18 | &global_[5] 'l' !< 19 | &global_[6] '\n' !< 20 | 21 | &sv.data &global_ !< 22 | &sv.len 7 !< 23 | 24 | &sv print 25 | 26 | return 27 | end 28 | 29 | func void test_array_local_character_word[] 30 | var local char[6] 31 | var sv string; 32 | 33 | &local[0] 'L' !< 34 | &local[1] 'o' !< 35 | &local[2] 'c' !< 36 | &local[3] 'a' !< 37 | &local[4] 'l' !< 38 | &local[5] '\n' !< 39 | 40 | &sv.data &local !< 41 | &sv.len 6 !< 42 | 43 | &sv print 44 | return 45 | end 46 | 47 | no_return func void main[] 48 | // TODO: Allow testkit to specify these cases as separate tests 49 | call test_array_global_character_word 50 | call test_array_local_character_word 51 | 52 | 0 exit // TODO: This must be resolved as this raises an segmentation fault if not within exit 53 | end -------------------------------------------------------------------------------- /libgofra/lexer/io/io.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from pathlib import Path 3 | from typing import IO 4 | 5 | from .exceptions import ( 6 | IOFileDoesNotExistsError, 7 | IOFileNotAnFileError, 8 | IOFileNotAnTextFileError, 9 | ) 10 | 11 | 12 | def open_source_file_line_stream(path: Path) -> Generator[str]: 13 | """Open IO for an text file with Gofra source code and yield each line from it until it ends. 14 | 15 | :raises IOFileDoesNotExistsError: File does not exists 16 | :raises IOFileNotAnFileError: File is not an file 17 | """ 18 | try: 19 | with open_source_file(path) as io: 20 | while line := io.readline(-1): 21 | yield line 22 | except UnicodeDecodeError as e: 23 | raise IOFileNotAnTextFileError(path=path) from e 24 | 25 | 26 | def open_source_file(path: Path) -> IO[str]: 27 | """Open IO for an text file with Gofra source code. 28 | 29 | :raises IOFileDoesNotExistsError: File does not exists 30 | :raises IOFileNotAnFileError: File is not an file 31 | """ 32 | if not path.exists(follow_symlinks=True): 33 | raise IOFileDoesNotExistsError(path=path) 34 | 35 | if not path.is_file(): 36 | raise IOFileNotAnFileError(path=path) 37 | 38 | return path.open( 39 | mode="rt", 40 | buffering=-1, 41 | encoding="utf-8", 42 | errors="strict", 43 | newline=None, 44 | ) 45 | -------------------------------------------------------------------------------- /gofra/cli/parser/builder.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | from gofra.cli.parser import groups 4 | 5 | 6 | def build_cli_parser(prog: str) -> ArgumentParser: 7 | """Get argument parser instance to parse incoming arguments.""" 8 | parser = ArgumentParser( 9 | description="Gofra Toolkit - CLI for working with Gofra programming language", 10 | usage=f"{prog} files... [options] [-h]", 11 | add_help=True, 12 | allow_abbrev=False, 13 | prog=prog, 14 | ) 15 | 16 | parser.add_argument( 17 | "source_files", 18 | help="Input source code files in Gofra to process (`.gof` files)", 19 | nargs="*", 20 | default=[], 21 | ) 22 | 23 | parser.add_argument( 24 | "--version", 25 | default=False, 26 | action="store_true", 27 | help="Show version info", 28 | ) 29 | 30 | parser.add_argument( 31 | "--repl", 32 | default=False, 33 | action="store_true", 34 | help="Step into REPL", 35 | ) 36 | 37 | groups.add_debug_group(parser) 38 | groups.add_target_group(parser) 39 | groups.add_output_group(parser) 40 | groups.add_preprocessor_group(parser) 41 | groups.add_cache_group(parser) 42 | groups.add_linker_group(parser) 43 | groups.add_optimizer_group(parser) 44 | groups.add_toolchain_debug_group(parser) 45 | groups.add_additional_group(parser) 46 | return parser 47 | -------------------------------------------------------------------------------- /libgofra/hir/initializer.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass(frozen=True, slots=True) 6 | class VariableIntArrayInitializerValue: 7 | """HIR value parsed from initializer for int array. 8 | 9 | Variable must be initialized with this value. 10 | """ 11 | 12 | default: int 13 | values: list[int] 14 | 15 | 16 | @dataclass(frozen=True, slots=True) 17 | class VariableStringPtrArrayInitializerValue: 18 | """HIR value parsed from initializer for *string array. 19 | 20 | Variable must be initialized with this value. 21 | """ 22 | 23 | default: int 24 | values: list[str] 25 | 26 | 27 | @dataclass(frozen=True, slots=True) 28 | class VariableIntFieldedStructureInitializerValue: 29 | """HIR value parsed from initializer for structure where all fields are integers and fulfilled.""" 30 | 31 | values: Mapping[str, int] 32 | 33 | 34 | @dataclass(frozen=True, slots=True) 35 | class VariableStringPtrInitializerValue: 36 | """HIR value parsed from initializer for string (e.g *string / string). 37 | 38 | Variable must be initialized with this value. 39 | """ 40 | 41 | string: str 42 | 43 | 44 | type T_AnyVariableInitializer = ( 45 | int 46 | | VariableIntArrayInitializerValue 47 | | VariableStringPtrInitializerValue 48 | | VariableStringPtrArrayInitializerValue 49 | | VariableIntFieldedStructureInitializerValue 50 | ) 51 | -------------------------------------------------------------------------------- /.github/workflows/testkit.yml: -------------------------------------------------------------------------------- 1 | name: Compiler Testkit 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | python-version: ['3.12'] 14 | os: [macos-latest, ubuntu-latest] # TODO: Add `windows-latest` when we start to support Windows 15 | fail-fast: false 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Test compiler bootstrap and host 29 | run: | 30 | python -m gofra --version 31 | 32 | 33 | # TODO: Add pong test if there is any easy way to install raylib 34 | - name: Run tests 35 | # Threads is set to 1 as it affects testing speed, but currently makes display of errors harder as they not linear 36 | # probably should be fixed at some point 37 | run: | 38 | python -m gofra.testkit --silent --fail-with-abnormal-exit-code --threads 1 -d ./tests 39 | 40 | - name: Run examples test 41 | run: | 42 | python -m gofra.testkit \ 43 | --build-only \ 44 | --silent \ 45 | -d ./examples \ 46 | -p "*_*.gof" \ 47 | -e 03_pong.gof -------------------------------------------------------------------------------- /gofra/cli/goals/version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from platform import platform, python_implementation, python_version 3 | from typing import NoReturn 4 | 5 | from gofra.cli.parser.arguments import CLIArguments 6 | from libgofra.feature_flags import ( 7 | FEATURE_ALLOW_FPU, 8 | FEATURE_RUNTIME_ARRAY_OOB_CHECKS, 9 | ) 10 | from libgofra.linker.command_composer import get_linker_command_composer_backend 11 | 12 | 13 | def cli_perform_version_goal(args: CLIArguments) -> NoReturn: 14 | """Perform version goal that display information about host and toolchain.""" 15 | linker_composer_backend = ( 16 | get_linker_command_composer_backend(args.target) 17 | .__qualname__.removeprefix("compose_") 18 | .removesuffix("_command") 19 | ) 20 | 21 | print("[Gofra toolchain]") 22 | print("Toolchain target (may be unavailable on host machine):") 23 | print(f"\tTriplet: {args.target.triplet}") 24 | print(f"\tArchitecture: {args.target.architecture}") 25 | print(f"\tOS: {args.target.operating_system}") 26 | print(f"\t(linker command composer: {linker_composer_backend})") 27 | print("Host machine:") 28 | print(f"\tPlatform: {platform()}") 29 | print(f"\tPython: {python_implementation()} {python_version()}") 30 | print("Features:") 31 | print(f"\tFEATURE_ALLOW_FPU = {FEATURE_ALLOW_FPU}") 32 | print(f"\tFEATURE_RUNTIME_ARRAY_OOB_CHECKS = {FEATURE_RUNTIME_ARRAY_OOB_CHECKS}") 33 | return sys.exit(0) 34 | -------------------------------------------------------------------------------- /docs/tutorial/stack-management-operators.md: -------------------------------------------------------------------------------- 1 | # Stack management operators 2 | 3 | For managing an stack you must have some less-or-more complex commands for example like `swap` or `rot`, language supports some of them. 4 | 5 | 6 | # SWAP 7 | Mnemonics: `a b -> b a` 8 | 9 | Swaps two arguments from stack, for example for reaching second argument 10 | 11 | Example 12 | ```gofra 13 | 3 2 - // 1 14 | 3 2 swap - // -1 15 | ``` 16 | 17 | # DROP 18 | Mnemonics: `a -> _` 19 | 20 | Drops element from stack 21 | 22 | Example: 23 | ```gofra 24 | 2 2 100 drop + // 4, and empty stack 25 | ``` 26 | 27 | # COPY 28 | Mnemonics: `a -> a a` 29 | 30 | Copies element from stack 31 | 32 | Example: 33 | ```gofra 34 | 2 copy // 2 2 on stack 35 | ``` 36 | 37 | # Tips and Tricks 38 | 39 | - `swap` is useful when initializing local variables by function arguments values. For example: 40 | ```gofra 41 | func int str2int[ 42 | *char[], 43 | int 44 | ] do 45 | var len int; &len swap !< 46 | var ptr *char[]; &ptr swap !< 47 | ... 48 | end 49 | ``` 50 | In this example, the `len` variable is initialized by the `int` value from the arguments and 51 | the `ptr` variable is initialized by the `*char[]` value from the arguments. 52 | - `copy` is useful when increasing or decreasing some value. For example: 53 | ``` 54 | var number int = 0; 55 | 56 | &number copy ?> 10 + !< // number = number + 10 57 | ``` 58 | -------------------------------------------------------------------------------- /lib/random.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Random number generator (PRNG) 3 | // Under the hood uses Xorshift algorithm: 4 | // https://en.wikipedia.org/wiki/Xorshift 5 | // ============================================== 6 | 7 | // Static global state of randomizer 8 | var std_prng_random_state int 9 | 10 | // Set an given number as seed (PRNG random state) 11 | func void set_random_seed[int seed] 12 | &std_prng_random_state seed !< 13 | end 14 | 15 | // Randomize current PRNG random state 16 | func void prng_randomize_state_xorshift[] 17 | // Uses Xorshift generator algorithm 18 | // &rs rs rs 13 << ^ !< 19 | // &rs rs rs 17 << ^ !< 20 | // &rs rs rs 5 << ^ !< 21 | &std_prng_random_state 22 | std_prng_random_state 23 | std_prng_random_state 13 << 24 | ^ 25 | !< 26 | 27 | &std_prng_random_state 28 | std_prng_random_state 29 | std_prng_random_state 17 << 30 | ^ 31 | !< 32 | 33 | &std_prng_random_state 34 | std_prng_random_state 35 | std_prng_random_state 5 << 36 | ^ 37 | !< 38 | end 39 | 40 | // Get random number from PRNG 41 | func int random[] 42 | call prng_randomize_state_xorshift 43 | 44 | std_prng_random_state return // TODO: Fix parser/lexer eats identifiers 45 | end 46 | 47 | // Get random number from PRNG withing range from 0 to argument 48 | func int random_range[int to] 49 | call random 50 | to % 51 | end -------------------------------------------------------------------------------- /libgofra/codegen/sections/_factory.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from typing import Protocol 3 | 4 | from libgofra.targets.target import Target 5 | 6 | from .elf import ( 7 | ELF_SECTION_BSS, 8 | ELF_SECTION_DATA, 9 | ELF_SECTION_INSTRUCTIONS, 10 | ELF_SECTION_STRINGS, 11 | ) 12 | from .macho import ( 13 | MACHO_SECTION_BSS, 14 | MACHO_SECTION_DATA, 15 | MACHO_SECTION_INSTRUCTIONS, 16 | MACHO_SECTION_STRINGS, 17 | ) 18 | 19 | 20 | class SectionType(Enum): 21 | BSS = auto() 22 | DATA = auto() 23 | STRINGS = auto() 24 | INSTRUCTIONS = auto() 25 | 26 | 27 | class Section(Protocol): 28 | def __str__(self) -> str: ... 29 | 30 | 31 | def get_os_assembler_section(section: SectionType, target: Target) -> Section: 32 | match target.operating_system: 33 | case "Linux": 34 | return { 35 | SectionType.BSS: ELF_SECTION_BSS, 36 | SectionType.DATA: ELF_SECTION_DATA, 37 | SectionType.INSTRUCTIONS: ELF_SECTION_INSTRUCTIONS, 38 | SectionType.STRINGS: ELF_SECTION_STRINGS, 39 | }[section] 40 | case "Darwin": 41 | return { 42 | SectionType.BSS: MACHO_SECTION_BSS, 43 | SectionType.DATA: MACHO_SECTION_DATA, 44 | SectionType.INSTRUCTIONS: MACHO_SECTION_INSTRUCTIONS, 45 | SectionType.STRINGS: MACHO_SECTION_STRINGS, 46 | }[section] 47 | case "Windows": 48 | raise NotImplementedError 49 | -------------------------------------------------------------------------------- /libgofra/linker/apple/libraries.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from functools import lru_cache 3 | from pathlib import Path 4 | from subprocess import run 5 | 6 | from libgofra.linker.apple.output_format import AppleLinkerOutputFormat 7 | 8 | # By default system library root is located here (`xcrun -sdk macosx --show-sdk-path`) 9 | SYSLIBROOT_DEFAULT_PATH = Path("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk") 10 | SYSLIBROOT_XCRUN_GET_COMMAND = ("/usr/bin/xcrun", "-sdk", "macosx", "--show-sdk-path") 11 | 12 | # We use explicit `-Z` flag so we explicitly propagate these search paths. 13 | APPLE_LINKER_DEFAULT_LIBRARIES_SEARCH_PATHS = [ 14 | Path("/usr/lib"), 15 | Path("/usr/local/lib"), 16 | ] 17 | 18 | 19 | @lru_cache(maxsize=1) 20 | def get_syslibroot_path() -> Path: 21 | """Get `syslibroot` path flag for linker. Do not call `xcrun` multiple times.""" 22 | if SYSLIBROOT_DEFAULT_PATH.exists(): 23 | return SYSLIBROOT_DEFAULT_PATH 24 | 25 | # TODO(@kirillzhosul): Add logging about performing `SYSLIBROOT_XCRUN_GET_COMMAND` 26 | 27 | process = run(SYSLIBROOT_XCRUN_GET_COMMAND, check=False) 28 | process.check_returncode() 29 | 30 | return Path(process.stdout.decode().strip()) 31 | 32 | 33 | def syslibroot_is_required( 34 | libraries: Iterable[str], 35 | output_format: AppleLinkerOutputFormat, 36 | ) -> bool: 37 | """Syslibroot is not required for non executables and not linking with system.""" 38 | _ = output_format 39 | return "System" in libraries 40 | -------------------------------------------------------------------------------- /libgofra/linker/command_composer.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable, MutableSequence 2 | from pathlib import Path 3 | from platform import system 4 | from typing import Protocol 5 | 6 | from libgofra.linker.apple.command_composer import compose_apple_linker_command 7 | from libgofra.linker.gnu.command_composer import compose_gnu_linker_command 8 | from libgofra.linker.output_format import LinkerOutputFormat 9 | from libgofra.linker.profile import LinkerProfile 10 | from libgofra.targets.target import Target 11 | 12 | 13 | class LinkerCommandComposer(Protocol): 14 | @staticmethod 15 | def __call__( # noqa: PLR0913 16 | objects: Iterable[Path], 17 | output: Path, 18 | target: Target, 19 | output_format: LinkerOutputFormat, 20 | libraries: MutableSequence[str], 21 | additional_flags: list[str], 22 | libraries_search_paths: list[Path], 23 | profile: LinkerProfile, 24 | executable_entry_point_symbol: str = ..., 25 | *, 26 | cache_directory: Path | None = None, 27 | linker_executable: Path | None = ..., 28 | ) -> list[str]: ... 29 | 30 | 31 | def get_linker_command_composer_backend( 32 | target: Target, # noqa: ARG001 33 | ) -> LinkerCommandComposer: 34 | """Gut linker command composer backend suitable for that target and current host.""" 35 | if system() == "Darwin": 36 | # Always use Apple linker on Darwin (e.g MacOS) 37 | return compose_apple_linker_command 38 | 39 | return compose_gnu_linker_command 40 | -------------------------------------------------------------------------------- /gofra/cli/parser/arguments.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | from typing import Literal 4 | 5 | from libgofra.linker.profile import LinkerProfile 6 | from libgofra.optimizer.config import OptimizerConfig 7 | from libgofra.targets.target import Target 8 | 9 | 10 | @dataclass(slots=True, frozen=True) 11 | class CLIArguments: 12 | """Arguments from argument parser provided for whole Gofra toolchain process.""" 13 | 14 | source_filepaths: list[Path] 15 | output_filepath: Path 16 | output_format: Literal["library", "object", "executable", "assembly"] 17 | output_file_is_specified: bool 18 | 19 | execute_after_compilation: bool 20 | debug_symbols: bool 21 | 22 | include_paths: list[Path] 23 | definitions: dict[str, str] 24 | 25 | version: bool 26 | repl: bool 27 | 28 | hir: bool 29 | preprocess_only: bool 30 | 31 | assembler_flags: list[str] 32 | 33 | verbose: bool 34 | 35 | target: Target 36 | 37 | skip_typecheck: bool 38 | 39 | build_cache_dir: Path 40 | delete_build_cache: bool 41 | 42 | linker_profile: LinkerProfile 43 | 44 | linker_additional_flags: list[str] 45 | linker_libraries: list[str] 46 | linker_libraries_search_paths: list[Path] 47 | linker_backend: Literal["gnu-ld", "apple-ld"] | None 48 | linker_resolve_libraries_with_pkgconfig: bool 49 | linker_executable: Path | None 50 | 51 | optimizer: OptimizerConfig 52 | lexer_debug_emit_lexemes: bool 53 | cli_debug_user_friendly_errors: bool 54 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Pre requirements 4 | 5 | Gofra is a Python-based toolchain, so you only require a Python installation. However, as a toolchain-compiled code, it depends on the target-specific linker and assembler. 6 | 7 | - [Python >3.12.x](https://www.python.org) available as `python` or `python3` command 8 | - GNU/Mach-O Linker (ld) - For linking compiled objects 9 | - Assembler (as) - Typically included with Clang LLVM compiler 10 | 11 | [Gofra](https://github.com/kirillzhosul/gofra) is distributed as single Python-based toolchain. To install: 12 | 13 | ## Install 14 | 15 | (Step 1): Install toolchain 16 | ```bash 17 | pip install gofra 18 | ``` 19 | 20 | (Step 2): Verify Installation 21 | ```bash 22 | gofra --help 23 | ``` 24 | 25 | 26 | ## Development installation 27 | 28 | (Step 1): Clone the repository 29 | ```bash 30 | git clone https://github.com/kirillzhosul/gofra.git 31 | cd gofra 32 | ``` 33 | 34 | (Step 2): Verify installation 35 | ```bash 36 | cd gofra 37 | python -m gofra --help 38 | ``` 39 | (Step 3): Install dependencies ([Poetry](https://python-poetry.org) required) 40 | ```bash 41 | # In repository root 42 | poetry install --with dev,docs 43 | ``` 44 | 45 | This will install [Ruff](https://astral.sh/ruff) and [MkDocs](https://www.mkdocs.org) available as: 46 | ```bash 47 | # Serve documentation 48 | mkdocs serve 49 | # Lint source code 50 | ruff . 51 | ``` 52 | 53 | (It will also propagate the local `gofra` command over the system-wide gofra if you are inside the environment (e.g. `poetry shell`)) 54 | 55 | -------------------------------------------------------------------------------- /gofra/execution/permissions.py: -------------------------------------------------------------------------------- 1 | """System level I/O permissions. 2 | 3 | (e.g Operating system permissions on filesystem) 4 | Useful for manipulating on an output executable from an toolchain 5 | """ 6 | 7 | import stat 8 | import sys 9 | from pathlib import Path 10 | 11 | 12 | def apply_file_executable_permissions(filepath: Path) -> None: 13 | """Change file permissions to make it executable in secure manner. 14 | 15 | :param filepath: Path to file 16 | :raises OSError: If path is not an file. 17 | :raises PermissionError: If it is an symlink or not enough permissions to change file mode 18 | """ 19 | if sys.platform == "win32": 20 | return 21 | 22 | if not filepath.is_file(): 23 | msg = f"Cannot apply file executable permissions for an non-file ({filepath}), this is probably an bug in the toolchain" 24 | raise OSError(msg) 25 | 26 | if filepath.is_symlink(): 27 | msg = "Cannot securely apply executable permission on symlink, this is security consideration and probably an bug in the toolchain" 28 | raise PermissionError(msg) 29 | 30 | # Securely append only execution for current user (owner) and others in his group 31 | executable_mode = stat.S_IXUSR | stat.S_IXGRP 32 | prohibit_mode = ~stat.S_IXOTH 33 | 34 | # Do not override permissions with new, append only possible new ones and remove insecure 35 | current_mode = filepath.stat().st_mode 36 | new_mode = (current_mode | executable_mode) & prohibit_mode 37 | filepath.chmod(mode=new_mode, follow_symlinks=False) 38 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/sections/bss_variables.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from libgofra.codegen.sections._factory import SectionType 6 | 7 | from ._alignment import get_type_data_alignment 8 | 9 | if TYPE_CHECKING: 10 | from collections.abc import Mapping 11 | 12 | from libgofra.codegen.backends.aarch64._context import AARCH64CodegenContext 13 | from libgofra.hir.variable import Variable 14 | from libgofra.types._base import Type 15 | 16 | 17 | def write_uninitialized_data_section( 18 | context: AARCH64CodegenContext, 19 | variables: Mapping[str, Variable[Type]], 20 | ) -> None: 21 | """Emit data section with uninitialized variables.""" 22 | uninitialized_variables = { 23 | k: v for k, v in variables.items() if v.initial_value is None 24 | } 25 | 26 | if not uninitialized_variables: 27 | return 28 | 29 | context.section(SectionType.BSS) 30 | aligned_by = 0 31 | for name, variable in uninitialized_variables.items(): 32 | type_size = variable.size_in_bytes 33 | assert type_size, f"Variables must have size (from {variable.name})" 34 | 35 | # TODO(@kirillzhosul): Align by specifications of type not general byte size 36 | alignment = get_type_data_alignment(variable.type) 37 | if alignment and alignment != aligned_by: 38 | aligned_by = alignment 39 | context.fd.write(f".p2align {alignment}\n") 40 | 41 | context.fd.write(f"{name}: .space {type_size}\n") 42 | -------------------------------------------------------------------------------- /libgofra/lexer/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from collections.abc import Callable 7 | 8 | 9 | ESCAPE_SYMBOL = "\\" 10 | 11 | 12 | def unescape_text_literal(string: str) -> str: 13 | """Remove all terminations within string/char (escape it).""" 14 | return string.encode().decode("unicode-escape") 15 | 16 | 17 | def find_word_start(text: str, start: int) -> int: 18 | """Find start column index of an word.""" 19 | return _find_column(text, start, lambda s: not s.isspace()) 20 | 21 | 22 | def find_word_end(text: str, start: int) -> int: 23 | """Find end column index of an word.""" 24 | return _find_column(text, start, lambda s: s.isspace()) 25 | 26 | 27 | def find_quoted_literal_end(line: str, idx: int, *, quote: str) -> int: 28 | """Find index where given string ends (close quote) or -1 if not closed properly.""" 29 | idx_end = len(line) 30 | 31 | prev = line[idx] if idx >= 0 and idx < len(line) else None 32 | while idx < idx_end: 33 | current = line[idx] 34 | if current == quote and prev != ESCAPE_SYMBOL: 35 | return idx + 1 36 | 37 | prev = current 38 | idx += 1 39 | 40 | return -1 41 | 42 | 43 | def _find_column(text: str, start: int, predicate: Callable[[str], bool]) -> int: 44 | """Find index of an column by predicate. E.g `.index()` but with predicate.""" 45 | end = len(text) 46 | while start < end and not predicate(text[start]): 47 | start += 1 48 | return start 49 | -------------------------------------------------------------------------------- /gofra/execution/execution.py: -------------------------------------------------------------------------------- 1 | """Execution helpers for filesystem binary files.""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | from subprocess import run 7 | from typing import TYPE_CHECKING, TextIO 8 | 9 | if TYPE_CHECKING: 10 | from collections.abc import Iterable 11 | from pathlib import Path 12 | 13 | 14 | def execute_binary_executable( # noqa: PLR0913 15 | filepath: Path, 16 | *, 17 | args: Iterable[str], 18 | timeout: float | None = None, 19 | stdin: TextIO | int = sys.stdin, 20 | stdout: TextIO | int = sys.stdout, 21 | stderr: TextIO | int = sys.stderr, 22 | ) -> int: 23 | """Execute given binary executable in an shell and return its exit code. 24 | 25 | :param filepath: Path to an executable 26 | :param args: Sequence of arguments to pass when executing 27 | :param timeout: Timeout in seconds 28 | :return: Exit code of an process 29 | :raises subprocess.TimeoutExpired: Timeout is expired 30 | """ 31 | executable = filepath.absolute() 32 | 33 | process = run( # noqa: S602 34 | (executable, *args), 35 | timeout=timeout, 36 | stdin=stdin, 37 | stdout=stdout, 38 | stderr=stderr, 39 | # Mostly, we want to execute with shell as less-abstracted real software 40 | shell=True, 41 | # Do not raise, we would do that ourselves 42 | check=False, 43 | # Perform in current environment 44 | user=None, 45 | group=None, 46 | process_group=None, 47 | ) 48 | 49 | return process.returncode 50 | -------------------------------------------------------------------------------- /docs/tutorial/structures.md: -------------------------------------------------------------------------------- 1 | # Structures 2 | 3 | Structs currently is an something like type-definition with proper type checking and field accessors (auto-shift) 4 | 5 | ## Defining an structure type 6 | 7 | ```gofra 8 | struct name 9 | field type 10 | ... 11 | end 12 | ``` 13 | 14 | For example defining an person struct 15 | ```gofra 16 | struct Person 17 | age int 18 | name *char[] 19 | end 20 | ``` 21 | 22 | ## Accessing an structure field 23 | ```gofra 24 | var stepan Person // initialize variable of type Person 25 | 26 | stepan.age // will push an pointer to an `age` field so you can write / read from it 27 | ``` 28 | 29 | ## Alignment on CPUs 30 | 31 | All structures are alignment by default to machine alignment 32 | 33 | 34 | ## Big Caveats 35 | 36 | - Structs are not passed according to C-FFI ABI 37 | - Structs have no constructors 38 | - ... and much more as this feature is being in development an mainly used as drop-in replacement to defining an structs with `char[32]` or `int[2]`... 39 | 40 | 41 | ## HIR perspective 42 | 43 | Accessing an structure field will as by default emit `PUSH_VARIABLE_ADDRESS` HIR operator which is followed by `STRUCT_FIELD_OFFSET` which is resolved to `ADDR(VAR) + FIELD_OFFSET(field)` so `*struct` resolve into `*struct.field` as auto type inference from structure type 44 | 45 | 46 | ## LIR perspective 47 | 48 | Structure type by default is an complex type with collection of field types and their ordering respectfully 49 | `STRUCT_FIELD_OFFSET` is an almost inline-assembly code generation which performs addition right inside runtime 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "gofra" 3 | version = "0.0.5" 4 | description = "A Stack-based compiled programming language" 5 | requires-python = ">=3.12,<4.0" 6 | license = "MIT" 7 | readme = "README.md" 8 | authors = [{ name = "Kirill Zhosul", email = "kirillzhosul@yandex.com" }] 9 | maintainers = [] 10 | keywords = ["compiler", "language"] 11 | 12 | [project.scripts] 13 | gofra = "gofra.cli.__main__:cli_entry_point" 14 | gofra-testkit = "gofra.testkit.__main__:main" 15 | 16 | [project.urls] 17 | documentation = "https://kirillzhosul.github.io/gofra" 18 | repository = "https://github.com/kirillzhosul/gofra" 19 | 20 | [tool.poetry] 21 | package-mode = true 22 | packages = [{ include = "gofra" }] 23 | include = ["gofra/_distlib/**/*.gof"] 24 | exclude = ["gofra/testkit"] 25 | 26 | [tool.poetry.group.docs.dependencies] 27 | mkdocs = "^1.6.1" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | ruff = "^0.13.1" 31 | pytest = "^9.0.1" 32 | 33 | [tool.ruff.lint] 34 | select = ["ALL"] 35 | ignore = [ 36 | "S101", 37 | "A005", 38 | "D105", 39 | "D100", 40 | "D103", 41 | "PLR0912", 42 | "PLR0915", 43 | "C901", 44 | "D107", 45 | "D101", 46 | "D102", 47 | "W291", 48 | "E501", 49 | "UP031", 50 | "T201", 51 | "S603", 52 | "D203", 53 | "D213", 54 | "FIX002", # do not warn on TODOs 55 | "TD003", # do not warn on TODOs 56 | "PLR2004", # We often use something like 8/16 bytes and mostly this is relevant for us 57 | ] 58 | 59 | [build-system] 60 | requires = ["poetry-core"] 61 | build-backend = "poetry.core.masonry.api" 62 | -------------------------------------------------------------------------------- /libgofra/assembler/__init__.py: -------------------------------------------------------------------------------- 1 | """Assembler package that translated *assembly* file into *object* file. 2 | 3 | For example in codegen that is not emitting final object file, we need to call an *assembler* that is translates our codegen result 4 | (e.g an `.s` assembly file with text machine instructions) into object file that is suitable for next possible linkage (linker) step 5 | 6 | 7 | For example in x86_64 (AMD64) toolchain target workflow is: 8 | - Gofra high-level toolchain (Lex -> parse IR (HIR)) 9 | - Pass that into Gofra codegen which may translate that HIR into LIR for internal code generation and translate that into an assembly file 10 | - We need to translate that assembly into an object file via that assembler tooling 11 | - [after emitting an object file we link it into final binary (e.g executable for most cases)] 12 | In matchup: Lexer (Tokens) -> Parser (HIR) -> Codegen (Assembly Text) -> Assembler (Object file) -> Linker (Binary) 13 | 14 | This is not required for targets / codegen that emits final *object* file (e.g any codegen that emits binary machine instructions in form of an object file) 15 | 16 | (Not exist in toolchain, only reference): 17 | In LLVM partial (llvm only as LIR) workflow we still receive LLVM IR (LIR) that needs translation to binary object file 18 | In LLVM full workflow we use `clang` to fully compile source file (codegen assembly -> executable) but this hides abstraction (which is mostly not used by language toolchain, but anyway) 19 | """ 20 | 21 | from .assembler import assemble_object_from_codegen_assembly 22 | 23 | __all__ = ["assemble_object_from_codegen_assembly"] 24 | -------------------------------------------------------------------------------- /libgofra/gofra.py: -------------------------------------------------------------------------------- 1 | """Gofra core entry.""" 2 | 3 | from collections.abc import Generator, Iterable 4 | from pathlib import Path 5 | 6 | from libgofra.hir.module import Module 7 | from libgofra.lexer import tokenize_from_raw 8 | from libgofra.lexer.io import open_source_file_line_stream 9 | from libgofra.lexer.tokens import Token 10 | from libgofra.parser.parser import parse_module_from_tokenizer 11 | from libgofra.preprocessor import preprocess_file 12 | from libgofra.preprocessor.macros.registry import MacrosRegistry 13 | 14 | 15 | def process_input_file( 16 | filepath: Path, 17 | include_paths: Iterable[Path], 18 | *, 19 | macros: MacrosRegistry, 20 | _debug_emit_lexemes: bool = False, 21 | ) -> Module: 22 | """Core entry for Gofra API. 23 | 24 | Compiles given filepath down to `IR` into `Module` (e.g translation unit). 25 | Maybe assembled into executable/library/object/etc... via `assemble_program` 26 | 27 | Does not provide optimizer or type checker. 28 | """ 29 | io = open_source_file_line_stream(filepath) 30 | lexer = tokenize_from_raw(filepath, lines=io) 31 | preprocessor = preprocess_file( 32 | filepath, 33 | lexer, 34 | include_paths, 35 | macros, 36 | ) 37 | 38 | if _debug_emit_lexemes: 39 | preprocessor = _debug_lexer_wrapper(preprocessor) 40 | 41 | return parse_module_from_tokenizer(filepath, preprocessor) 42 | 43 | 44 | def _debug_lexer_wrapper(lexer: Generator[Token]) -> Generator[Token]: 45 | for token in lexer: 46 | print(token.type.name, token.value, token.location) 47 | yield token 48 | -------------------------------------------------------------------------------- /gofra/cli/goals/__init__.py: -------------------------------------------------------------------------------- 1 | """Goals for CLI (e.g compile, show version) as different goals that output different result.""" 2 | 3 | import sys 4 | from time import perf_counter_ns 5 | from typing import NoReturn 6 | 7 | from gofra.cli.goals.compile import cli_perform_compile_goal 8 | from gofra.cli.goals.hir import cli_perform_hir_goal 9 | from gofra.cli.goals.preprocessor import cli_perform_preprocess_goal 10 | from gofra.cli.goals.repl import cli_perform_repl_goal 11 | from gofra.cli.goals.version import cli_perform_version_goal 12 | from gofra.cli.output import cli_message 13 | from gofra.cli.parser.arguments import CLIArguments 14 | 15 | NANOS_TO_SECONDS = 1_000_000_000 16 | 17 | 18 | def perform_desired_toolchain_goal(args: CLIArguments) -> NoReturn: 19 | """Perform toolchain goal base on CLI arguments, by default fall into compile goal.""" 20 | start = perf_counter_ns() 21 | try: 22 | if args.version: 23 | return cli_perform_version_goal(args) 24 | 25 | if args.repl: 26 | return cli_perform_repl_goal(args) 27 | 28 | if args.preprocess_only: 29 | return cli_perform_preprocess_goal(args) 30 | 31 | if args.hir: 32 | return cli_perform_hir_goal(args) 33 | 34 | return cli_perform_compile_goal(args) 35 | except SystemExit as e: 36 | end = perf_counter_ns() 37 | time_taken = (end - start) / NANOS_TO_SECONDS 38 | cli_message( 39 | "INFO", 40 | f"Performing an goal took {time_taken:.2f} seconds!", 41 | verbose=args.verbose, 42 | ) 43 | sys.exit(e.code) 44 | -------------------------------------------------------------------------------- /libgofra/hir/module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from collections.abc import MutableMapping 8 | from pathlib import Path 9 | 10 | from libgofra.hir.function import Function 11 | from libgofra.hir.variable import Variable 12 | from libgofra.types._base import Type 13 | from libgofra.types.composite.structure import StructureType 14 | 15 | 16 | @dataclass(frozen=True, slots=True) 17 | class Module: 18 | """HIR compilation (parsing) unit or module for an preprocessed (is applicable) file. 19 | 20 | Contains definitions from that preprocessed unit/module file. 21 | (Using unit terminology as an analogy for C/C++ translation/compilation units) 22 | """ 23 | 24 | # Location of an file which this module initial parsed from (excluding preprocessor stages) 25 | path: Path 26 | 27 | # Any function that this module defines (implements, externs, exports) 28 | functions: MutableMapping[str, Function] 29 | 30 | # Global functions that this module defines (excluding static variables inside function) 31 | # notice that static variables inside function is held inside functions 32 | variables: MutableMapping[str, Variable[Type]] 33 | 34 | # Structures that this module defines (can be unused) 35 | structures: MutableMapping[str, StructureType] 36 | 37 | @property 38 | def executable_functions(self) -> filter[Function]: 39 | return filter( 40 | lambda f: not f.is_external, 41 | self.functions.values(), 42 | ) 43 | -------------------------------------------------------------------------------- /docs/advanced/typechecker.md: -------------------------------------------------------------------------------- 1 | # Type checker 2 | 3 | 4 | Type checking is always performed by default at compile-time (unless `-nt` or `--no-typecheck` flag is passed) 5 | It validates all stack usages and function calls 6 | 7 | 8 | ## Implementation 9 | 10 | Type checker is implemented as type-stack machine which behaves like stack based machine but operates on types 11 | So for example `2` will resolve into `typestack = [INT]` 12 | 13 | Typechecker will perform type safety validation even for unused functions (expect function calls, as there is no calls for unused functions) 14 | Unless you apply [DCE](./optimizations.md) optimization at compile-time (as it performed before type checker stage) 15 | 16 | Type checker will go into each function and validate its return type and stack usage and emit an error if something weird is found 17 | 18 | When typechecker found an call inside current function, it will consume desired parameters as arguments from type stack and push return type unless it not `void` type 19 | 20 | ## Type comparison 21 | 22 | Typechecker always uses strategy called `strict-same-type` which means types must be always same and no inference / type lowering is possible (you must always statically type cast for fixing errors) 23 | Possible next update will introduce something new, allowing to use `implicit-byte-size` strategy or any type lowering automatically at typechecker stage 24 | 25 | ## Is type checker affects runtime / generate code? 26 | 27 | Type checker is meant to only type-safety feature, so no generated code is affected by typechecker, even type casts are static ones and only applied to type checker, not anything else -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/registers.py: -------------------------------------------------------------------------------- 1 | """Consts and types related to AARCH64 registers and architecture (including FFI/ABI/IPC).""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Literal 6 | 7 | #### 8 | # Bare AARCH64 related 9 | #### 10 | 11 | # Stack is alignment to specified bytes count 12 | # Each cell of an stack must be within that size 13 | # Pushing >2 cells onto stack will lead to cell overflow due to language stack nature. 14 | AARCH64_STACK_ALIGNMENT = 16 15 | AARCH64_STACK_ALIGNMENT_BIN = 4 # 2 ** 4 -> AARCH64_STACK_ALIGNMENT 16 | 17 | # Bits count (size) for different word types 18 | AARCH64_HALF_WORD_BITS = 0xFFFF # 4 bytes (16 bits) 19 | AARCH64_DOUBLE_WORD_BITS = 0xFFFF_FFFF_FFFF_FFFF # 16 bytes (64 bits) 20 | 21 | # Registers specification for AARCH64 22 | # Skips some of registers (X8-X15, X18-X30) due to currently being unused 23 | type AARCH64_ABI_X_REGISTERS = Literal["X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7"] 24 | type AARCH64_ABI_W_REGISTERS = Literal["W0", "W1", "W2", "W3", "W4", "W5", "W6", "W7"] 25 | type AARCH64_ABI_D_REGISTERS = Literal["D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7"] 26 | type AARCH64_ABI_ZERO_REGISTERS = Literal["WZR", "XZR"] 27 | type AARCH64_SP_REGISTER = Literal["SP"] 28 | type AARCH64_ABI_REGISTERS = ( 29 | AARCH64_ABI_X_REGISTERS | AARCH64_ABI_W_REGISTERS | AARCH64_ABI_D_REGISTERS 30 | ) 31 | type AARCH64_IPC_REGISTERS = Literal["X16", "X17"] 32 | type AARCH64_ZERO_REGISTERS = Literal["WZR", "XZR"] 33 | type AARCH64_GP_REGISTERS = AARCH64_ABI_REGISTERS | AARCH64_IPC_REGISTERS 34 | 35 | STACK_POINTER = "SP" 36 | FRAME_POINTER = "X29" 37 | LINK_REGISTER = "X30" 38 | -------------------------------------------------------------------------------- /libgofra/codegen/sections/elf.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from dataclasses import dataclass 3 | from typing import Literal 4 | 5 | 6 | @dataclass(frozen=True, slots=True) 7 | class ELFSection: 8 | """Specification for ELF assembler section directives.""" 9 | 10 | name: Literal[".bss", ".data", ".rodata", ".text"] 11 | type: Literal["@progbits", "@nobits"] 12 | flags: Sequence[ 13 | Literal[ 14 | "a", # alloc - occupies memory during execution 15 | "w", # write - writable data 16 | "x", # execute - executable instructions 17 | "M", # merge - can be merged (for strings) 18 | "S", # strings - contains null-terminated strings 19 | ] 20 | ] 21 | 22 | def __str__(self) -> str: 23 | """Get ELF section directive for assembler.""" 24 | parts = [self.name] 25 | 26 | parts.append(f'"{"".join(self.flags)}"') 27 | 28 | if self.type: 29 | parts.append(self.type) 30 | 31 | return ", ".join(parts) 32 | 33 | 34 | ELF_SECTION_BSS = ELFSection( 35 | name=".bss", 36 | flags=("a", "w"), 37 | type="@nobits", 38 | ) # Uninitialized variables 39 | 40 | ELF_SECTION_DATA = ELFSection( 41 | name=".data", 42 | flags=("a", "w"), 43 | type="@progbits", 44 | ) # Initialized read-write data 45 | 46 | ELF_SECTION_STRINGS = ELFSection( 47 | name=".rodata", 48 | flags=("a", "S", "M"), 49 | type="@progbits", 50 | ) # Read-only data (strings, constants) 51 | 52 | ELF_SECTION_INSTRUCTIONS = ELFSection( 53 | name=".text", 54 | flags=("a", "x"), 55 | type="@progbits", 56 | ) # Executable code 57 | -------------------------------------------------------------------------------- /editors/vscode/syntaxes/gofra.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "source.gofra", 3 | "patterns": [ 4 | { 5 | "name": "keyword.control.gofra", 6 | "match": "\\b(if|for|in|while|end|extern|global|inline|func|do|var|typecast|return|struct|sizeof|type|const)\\b" 7 | }, 8 | { 9 | "name": "keyword.preprocessor.gofra", 10 | "match": "(#macro|#define|#include|#endif|#ifdef|#ifndef)" 11 | }, 12 | { 13 | "name": "keyword.operator.gofra", 14 | "match": "(==|!=|<=|>=|<|>|&&)" 15 | }, 16 | { 17 | "name": "string.quoted.double.gofra", 18 | "begin": "\"", 19 | "end": "\"", 20 | "patterns": [ 21 | { 22 | "name": "constant.character.escape.gofra", 23 | "match": "\\\\." 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "keyword.other.gofra", 29 | "match": "\\b(drop|swap|call|copy|breakpoint|syscall0|syscall1|syscall2|syscall3|syscall4|syscall5|syscall6)\\b" 30 | }, 31 | { 32 | "name": "comment.line.gofra", 33 | "match": "//.*$" 34 | }, 35 | { 36 | "name": "punctuation.definition.parameters.begin.gofra", 37 | "match": "\\[" 38 | }, 39 | { 40 | "name": "punctuation.definition.parameters.end.gofra", 41 | "match": "\\]" 42 | }, 43 | { 44 | "name": "punctuation.separator.parameter.gofra", 45 | "match": "," 46 | }, 47 | { 48 | "name": "storage.type.gofra", 49 | "match": "\\b(int|void|bool|char|i64)\\b" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /libgofra/linker/apple/output_format.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class AppleLinkerOutputFormat(Enum): 5 | """Formats of output that Apple Linker may emit after linkage process.""" 6 | 7 | # Mach-O main executable 8 | EXECUTABLE = auto() # MH_EXECUTE 9 | 10 | # Mach-O shared library 11 | SHARED_LIBRARY = auto() # MH_DYLIB 12 | 13 | # Merge object files into single one 14 | OBJECT_FILE = auto() # MH_OBJECT 15 | 16 | # Notice: 17 | # Only formats above is used (or should be used) 18 | # formats below is some non-general and non cross-platform Darwin/MacOS specific type of shit 19 | 20 | # Mach-O bundle 21 | # Note (@kirillzhosul): this is some weird type of output 22 | # maybe sometime we will use that but at current moment there is no need in that 23 | BUNDLE = auto() # MH_BUNDLE 24 | 25 | # Mach-O dylinker 26 | # Notice: only used when building dyld 27 | # probably, should not be used inside our toolchain 28 | DYLINKER = auto() # MH_DYLINKER 29 | 30 | 31 | class AppleLinkerOutputFormatKindFlag(Enum): 32 | """Additional kind flags for `AppleLinkerOutputFormat`.""" 33 | 34 | # Default one (implied by underling `ld` tool by `EXECUTABLE`, `BUNDLE`, `SHARED_LIBRARY`) 35 | DYNAMIC = auto() 36 | 37 | # Does not use `dyld`. 38 | # Notice: Only used building kernel 39 | # probably, should not be used inside our toolchain 40 | STATIC = auto() 41 | 42 | # Produces a mach-o file in which the mach_header, load commands, and symbol table are not in any segment 43 | # This output type is used for firmware or embedded development where the segments are copied out of the mach-o into ROM/Flash. 44 | # Notice: 45 | # probably, should not be used inside our toolchain 46 | PRELOAD = auto() 47 | -------------------------------------------------------------------------------- /libgofra/lexer/literals/string.py: -------------------------------------------------------------------------------- 1 | """Lexer support for string literals.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from libgofra.lexer.errors import UnclosedStringQuoteError 8 | from libgofra.lexer.helpers import ( 9 | find_quoted_literal_end, 10 | find_word_start, 11 | unescape_text_literal, 12 | ) 13 | from libgofra.lexer.tokens import Token, TokenType 14 | 15 | if TYPE_CHECKING: 16 | from libgofra.lexer._state import LexerState 17 | 18 | 19 | STRING_QUOTE = '"' 20 | 21 | 22 | def tokenize_string_literal(state: LexerState) -> Token: 23 | """Tokenize string quote at cursor into string literal token or raise error if invalid string.""" 24 | assert state.line and state.line[state.col] == STRING_QUOTE, ( # noqa: PT018 25 | f"{tokenize_string_literal.__name__} must be called when cursor is at open string quote" 26 | ) 27 | 28 | location = state.current_location() 29 | state.col += len(STRING_QUOTE) 30 | 31 | ends_at = _find_string_literal_end(state) 32 | if ends_at == -1: 33 | raise UnclosedStringQuoteError(location) 34 | 35 | # Preserve literal as "abc" and unescaped version 36 | # TODO(@kirillzhosul): String literal escape messed when trying to escape last quote 37 | string_literal = state.line[state.col - 1 : ends_at] 38 | string_text = unescape_text_literal(string_literal.strip(STRING_QUOTE)) 39 | 40 | # Advance to next word 41 | state.col = find_word_start(state.line, ends_at) 42 | return Token( 43 | type=TokenType.STRING, 44 | text=string_literal, 45 | value=string_text, 46 | location=location, 47 | ) 48 | 49 | 50 | def _find_string_literal_end(state: LexerState) -> int: 51 | return find_quoted_literal_end(state.line, state.col, quote=STRING_QUOTE) 52 | -------------------------------------------------------------------------------- /libgofra/preprocessor/macros/exceptions.py: -------------------------------------------------------------------------------- 1 | from libgofra.lexer.tokens import Token, TokenLocation 2 | from libgofra.preprocessor.exceptions import PreprocessorError 3 | 4 | 5 | class PreprocessorMacroRedefinesLanguageWordError(PreprocessorError): 6 | def __init__(self, location: TokenLocation, name: str) -> None: 7 | self.location = location 8 | self.name = name 9 | 10 | def __repr__(self) -> str: 11 | return f"""Macro '{self.name}' at {self.location} tries to redefine language word-definition!""" 12 | 13 | 14 | class PreprocessorMacroRedefinedError(PreprocessorError): 15 | def __init__( 16 | self, 17 | name: str, 18 | redefined: TokenLocation, 19 | original: TokenLocation, 20 | ) -> None: 21 | self.name = name 22 | self.redefined = redefined 23 | self.original = original 24 | 25 | def __repr__(self) -> str: 26 | return f"""Redefinition of an macro '{self.name}' at {self.redefined} 27 | 28 | Original definition found at {self.original}. 29 | 30 | Only single definition allowed for macros. 31 | If it possible scenario of overriding, please un-define before redefinition.""" 32 | 33 | 34 | class PreprocessorMacroNonIdentifierNameError(PreprocessorError): 35 | def __init__(self, token: Token) -> None: 36 | self.token = token 37 | 38 | def __repr__(self) -> str: 39 | return f"""Non-identifier name for macro at {self.token.location}! 40 | 41 | Macros should have name as 'identifier' but got '{self.token.type.name}'!""" 42 | 43 | 44 | class PreprocessorNoMacroNameError(PreprocessorError): 45 | def __init__(self, location: TokenLocation) -> None: 46 | self.location = location 47 | 48 | def __repr__(self) -> str: 49 | return f"""No macro name specified at {self.location}! 50 | 51 | Do you have unfinished macro definition?""" 52 | -------------------------------------------------------------------------------- /libgofra/types/composite/structure.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | 3 | from libgofra.types._base import CompositeType, Type 4 | 5 | 6 | class StructureType(CompositeType): 7 | """Type that holds fields with their types as structure.""" 8 | 9 | # Direct mapping from name of the field to its direct type 10 | fields: Mapping[str, Type] 11 | 12 | # Order of name for offsetting in `fields`, as mapping does not contain any order information 13 | fields_ordering: Sequence[str] 14 | 15 | name: str 16 | size_in_bytes: int 17 | 18 | cpu_alignment_in_bytes: int 19 | 20 | def __init__( 21 | self, 22 | name: str, 23 | fields: Mapping[str, Type], 24 | fields_ordering: Sequence[str], 25 | cpu_alignment_in_bytes: int, 26 | ) -> None: 27 | self.name = name 28 | self.fields = fields 29 | self.fields_ordering = fields_ordering 30 | self.cpu_alignment_in_bytes = cpu_alignment_in_bytes 31 | self.recalculate_size_in_bytes() 32 | 33 | def recalculate_size_in_bytes(self) -> None: 34 | self.size_in_bytes = sum(f.size_in_bytes for f in self.fields.values()) 35 | 36 | # Align whole structure by alignment 37 | if self.cpu_alignment_in_bytes != 0: 38 | alignment = self.cpu_alignment_in_bytes + 1 39 | self.size_in_bytes = (self.size_in_bytes + alignment) & ~alignment 40 | 41 | def __repr__(self) -> str: 42 | return f"Struct {self.name}" 43 | 44 | def get_field_offset(self, field: str) -> int: 45 | """Get byte offset for given field (to access this field).""" 46 | offset_shift = 0 47 | for _field in self.fields_ordering: 48 | if _field == field: 49 | break 50 | offset_shift += self.fields[_field].size_in_bytes 51 | return offset_shift 52 | -------------------------------------------------------------------------------- /libgofra/codegen/backends/aarch64/static_data_section.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from libgofra.codegen.backends.aarch64.sections.bss_variables import ( 6 | write_uninitialized_data_section, 7 | ) 8 | from libgofra.codegen.backends.aarch64.sections.data_variables import ( 9 | write_initialized_data_section, 10 | ) 11 | from libgofra.codegen.backends.aarch64.sections.text_strings import ( 12 | write_text_string_section, 13 | ) 14 | 15 | if TYPE_CHECKING: 16 | from collections.abc import Mapping 17 | 18 | from libgofra.codegen.backends.aarch64._context import AARCH64CodegenContext 19 | from libgofra.hir.module import Module 20 | from libgofra.hir.variable import Variable 21 | from libgofra.types._base import Type 22 | 23 | 24 | def aarch64_data_section( 25 | context: AARCH64CodegenContext, 26 | program: Module, 27 | ) -> None: 28 | """Write program static data section filled with static strings and memory blobs.""" 29 | initialize_static_data_section( 30 | context, 31 | static_strings=context.strings, 32 | static_variables=program.variables, 33 | ) 34 | 35 | 36 | def initialize_static_data_section( 37 | context: AARCH64CodegenContext, 38 | static_strings: Mapping[str, str], 39 | static_variables: Mapping[str, Variable[Type]], 40 | ) -> None: 41 | """Initialize data section fields with given values. 42 | 43 | Section is an tuple (label, data) 44 | Data is an string (raw ASCII) or number (zeroed memory blob) 45 | """ 46 | write_uninitialized_data_section(context, static_variables) 47 | write_initialized_data_section(context, static_strings, static_variables) 48 | # Must defined after others - string initializer may forward reference them 49 | # but we define them by single-pass within writing initializer 50 | write_text_string_section(context, static_strings, reference_suffix="d") 51 | -------------------------------------------------------------------------------- /examples/04_http_server.gof: -------------------------------------------------------------------------------- 1 | 2 | // ============================================== 3 | // HTTP server complex example 4 | // Open an HTTP server on localhost that responds with simple HTML page 5 | // ============================================== 6 | 7 | // TODO!!!!!: This example at some point has broken! 8 | // TODO: Remove and feature `pack_sockaddr` 9 | 10 | #include "std.gof" 11 | #include "os/network.gof" 12 | #include "http" 13 | 14 | // TODO: Some static structs are invariant by default 15 | var server HTTPServer 16 | var request HTTPIncomingRequest 17 | 18 | func void pack_sockaddr[] 19 | &server.net_addr 20 | AF_INET 48 << // sin_family highest bit -> little endian 21 | 0x1F90 32 << // sin_port 22 | | 0x7F000001 | // sin_addr 23 | !< 24 | 25 | // zero padding by default but add `addr.sin_zero 0 !<` after fix 26 | end 27 | 28 | func void handle_request[] 29 | "Got connection!" println 30 | 31 | // Read request 32 | &request call http_server_read_request 33 | "Bytes read: " print print_int "\n" print 34 | 35 | // Send response 36 | &request 37 | "

Hello from Gofra HTTP server!

Built with Gofra and native HTTP library" 38 | call http_server_send_html_response 39 | "Bytes written: " print print_int "\n" print 40 | 41 | // Finish request 42 | &request call http_server_close_request 43 | "Closed connection!" println 44 | end 45 | 46 | func void main[] 47 | &server.port 8080 !< // TODO: not used 48 | &server.backlog 5 !< 49 | 50 | call pack_sockaddr 51 | &server call http_server_start 52 | 53 | "Listening..." println 54 | 55 | while true do 56 | &request.socket 57 | server.socket call net_accept_sink 58 | !< 59 | 60 | request.socket -1 == if 61 | "Unable to accept to client socket on HTTP server socket\n" eprint_fatal 62 | end 63 | 64 | call handle_request 65 | end 66 | end -------------------------------------------------------------------------------- /libgofra/lexer/tests/test_string_literals.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from libgofra.lexer._state import LexerState 4 | from libgofra.lexer.errors.unclosed_string_quote import UnclosedStringQuoteError 5 | from libgofra.lexer.literals.string import tokenize_string_literal 6 | from libgofra.lexer.tokens import Token, TokenType 7 | 8 | 9 | def test_string_literal_tokenize_empty_state() -> None: 10 | state = LexerState(path="toolchain") 11 | with pytest.raises(AssertionError): 12 | tokenize_string_literal(state) 13 | 14 | 15 | def test_string_literal_tokenize_valid() -> None: 16 | state = LexerState(path="toolchain") 17 | state.set_line(0, '"string"') 18 | _assert_is_string_token(tokenize_string_literal(state)) 19 | 20 | 21 | def test_string_literal_tokenize_unclosed() -> None: 22 | state = LexerState(path="toolchain") 23 | state.set_line(0, '"string') 24 | 25 | with pytest.raises(UnclosedStringQuoteError): 26 | tokenize_string_literal(state) 27 | 28 | 29 | def test_string_literal_tokenize_unclosed_eol() -> None: 30 | state = LexerState(path="toolchain") 31 | state.set_line(0, '"') 32 | 33 | with pytest.raises(UnclosedStringQuoteError): 34 | tokenize_string_literal(state) 35 | 36 | 37 | def test_string_literal_tokenize_empty() -> None: 38 | state = LexerState(path="toolchain") 39 | state.set_line(0, '""') 40 | _assert_is_string_token(tokenize_string_literal(state)) 41 | 42 | 43 | def test_string_literal_tokenize_escaped() -> None: 44 | state = LexerState(path="toolchain") 45 | state.set_line(0, r'"\r\n"') 46 | _assert_is_string_token(tokenize_string_literal(state)) 47 | 48 | 49 | @pytest.mark.skip("Does not work") 50 | def test_string_literal_tokenize_escaped_quote() -> None: 51 | state = LexerState(path="toolchain") 52 | state.set_line(0, r'"\""') 53 | _assert_is_string_token(tokenize_string_literal(state)) 54 | 55 | 56 | def _assert_is_string_token(t: Token) -> None: 57 | assert t.type == TokenType.STRING 58 | -------------------------------------------------------------------------------- /gofra/cli/goals/preprocessor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import TYPE_CHECKING, NoReturn 5 | 6 | from gofra.cli.output import cli_fatal_abort 7 | from libgofra.lexer import tokenize_from_raw 8 | from libgofra.lexer.io.io import open_source_file_line_stream 9 | from libgofra.lexer.tokens import TokenLocation 10 | from libgofra.preprocessor.macros.registry import registry_from_raw_definitions 11 | from libgofra.preprocessor.preprocessor import preprocess_file 12 | 13 | if TYPE_CHECKING: 14 | from gofra.cli.parser.arguments import CLIArguments 15 | 16 | 17 | def cli_perform_preprocess_goal(args: CLIArguments) -> NoReturn: 18 | """Perform preprocess only goal that emits preprocessed tokens into stdout.""" 19 | assert args.preprocess_only, ( 20 | "Cannot perform preprocessor goal with no preprocessor flag set!" 21 | ) 22 | 23 | if args.output_file_is_specified: 24 | return cli_fatal_abort( 25 | text="Output file has no effect for preprocess only goal, please pipe output via posix pipe (`>`) into desired file!", 26 | ) 27 | 28 | if len(args.source_filepaths) > 1: 29 | return cli_fatal_abort( 30 | text="Multiple source files has not effect for preprocess only goal, as it has no linkage, please specify single file!", 31 | ) 32 | 33 | macros_registry = registry_from_raw_definitions( 34 | location=TokenLocation.cli(), 35 | definitions=args.definitions, 36 | ).inject_propagated_defaults(target=args.target) 37 | 38 | path = args.source_filepaths[0] 39 | io = open_source_file_line_stream(path) 40 | lexer = tokenize_from_raw(path, io) 41 | preprocessor = preprocess_file( 42 | args.source_filepaths[0], 43 | lexer, 44 | args.include_paths, 45 | macros=macros_registry, 46 | ) 47 | 48 | for token in preprocessor: 49 | token_text = str(token.text) 50 | print(token_text, end=" ") 51 | 52 | return sys.exit(0) 53 | -------------------------------------------------------------------------------- /lib/math.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // Math helpers 3 | // E.g power, min/max, clamp, factorials 4 | // ============================================== 5 | 6 | // TODO: Gofra does not have FP so this is only integer-math library 7 | 8 | // Calculate base in power of exponent 9 | func int pow[int base, int exponent] 10 | var result int = 1; 11 | 12 | // TODO: This is not performant way to calculate power, but this is enough for now 13 | while exponent 0 > do 14 | &result result base * !< 15 | &exponent exponent 1 - !< 16 | end 17 | 18 | result return 19 | end 20 | 21 | // Calculate integer square root for an value (e.g floor Babylonian method) 22 | func int sqrt[int n] 23 | var y int = 1; 24 | var x int; 25 | 26 | // TODO: X64/AARCH64 has direct machine ops to perform FSQRT, probably at some time we introduce injected SQRT when done with FPU/FP and need fast SQRT (e.g 3D renderers) 27 | &x n !< // x = n 28 | 29 | while x y > do 30 | &x x y + 2 / !< 31 | &y n x / !< 32 | end 33 | 34 | x return 35 | end 36 | 37 | // Return absolute value of an negative/positive integer 38 | func int abs[int x] 39 | x 0 < if 40 | x -1 * return // (-n) * -1 => n 41 | end 42 | x return // Positive by default 43 | end 44 | 45 | // Return minimum value from two values 46 | func int min[int a, int b] 47 | a b <= if a return end 48 | b return 49 | end 50 | 51 | // Return maximals value from two values 52 | func int max[int a, int b] 53 | a b >= if a return end 54 | b return 55 | end 56 | 57 | // Clamp given value between two constraints 58 | func int clamp[int n, int a, int b] 59 | n a < if a return end 60 | n b > if b return end 61 | n return 62 | end 63 | 64 | // Factorial (n!) of value 65 | func int factorial[int n] 66 | var r = 1; 67 | var i = 2; 68 | 69 | while i n <= do 70 | &r r i * !< 71 | &i i 1 + !< 72 | end 73 | 74 | r return 75 | end 76 | -------------------------------------------------------------------------------- /libgofra/types/composite/string.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | 3 | from libgofra.types._base import Type 4 | from libgofra.types.composite.array import ArrayType 5 | from libgofra.types.composite.pointer import PointerType 6 | from libgofra.types.composite.structure import StructureType 7 | from libgofra.types.primitive.character import CharType 8 | from libgofra.types.primitive.integers import I64Type 9 | 10 | 11 | class StringType(StructureType): 12 | """Type that holds string as an slice of CString.""" 13 | 14 | # Direct mapping from name of the field to its direct type 15 | fields: Mapping[str, Type] = { 16 | "data": PointerType( 17 | ArrayType( 18 | element_type=CharType(), 19 | elements_count=0, 20 | ), 21 | ), 22 | "len": I64Type(), 23 | } 24 | 25 | # Order of name for offsetting in `fields`, as mapping does not contain any order information 26 | fields_ordering: Sequence[str] = ["data", "len"] 27 | 28 | name: str = "String" 29 | size_in_bytes: int 30 | 31 | cpu_alignment_in_bytes: int = 8 32 | 33 | def __init__(self) -> None: 34 | self.recalculate_size_in_bytes() 35 | 36 | def recalculate_size_in_bytes(self) -> None: 37 | self.size_in_bytes = sum(f.size_in_bytes for f in self.fields.values()) 38 | 39 | # Align whole structure by alignment 40 | if self.cpu_alignment_in_bytes != 0: 41 | alignment = self.cpu_alignment_in_bytes + 1 42 | self.size_in_bytes = (self.size_in_bytes + alignment) & ~alignment 43 | 44 | def __repr__(self) -> str: 45 | return self.name 46 | 47 | def get_field_offset(self, field: str) -> int: 48 | """Get byte offset for given field (to access this field).""" 49 | offset_shift = 0 50 | for _field in self.fields_ordering: 51 | if _field == field: 52 | break 53 | offset_shift += self.fields[_field].size_in_bytes 54 | return offset_shift 55 | -------------------------------------------------------------------------------- /libgofra/assembler/clang.py: -------------------------------------------------------------------------------- 1 | # Path to binary with clang (e.g `CC`) 2 | # Both Apple Clang / base Clang is allowed 3 | from collections.abc import Iterable 4 | from pathlib import Path 5 | from platform import system 6 | from shutil import which 7 | from typing import assert_never 8 | 9 | from libgofra.targets.target import Target 10 | 11 | CLANG_EXECUTABLE_PATH = Path("/usr/bin/clang") 12 | 13 | 14 | def compose_clang_assembler_command( # noqa: PLR0913 15 | machine_assembly_file: Path, 16 | output_object_file: Path, 17 | target: Target, 18 | *, 19 | additional_assembler_flags: Iterable[str], 20 | debug_information: bool, 21 | verbose: bool = False, 22 | ) -> list[str]: 23 | """Compose command for clang driver to be used as assembler.""" 24 | command = [str(CLANG_EXECUTABLE_PATH)] 25 | 26 | match target.triplet: 27 | case "arm64-apple-darwin" | "amd64-unknown-linux" | "amd64-unknown-windows": 28 | clang_target = target.triplet 29 | case _: 30 | assert_never(target.triplet) 31 | 32 | command.extend(("-target", clang_target)) 33 | 34 | # Treat and pass input as an assembly file - we are in assembler module 35 | # and use Clang only as assembler 36 | command.extend(("-c", "-x", "assembler", str(machine_assembly_file))) 37 | 38 | # Remove 39 | command.append("-nostdlib") 40 | 41 | if debug_information: 42 | command.append("-g") 43 | 44 | # Emit all commands and information that clang runs 45 | if verbose: 46 | command.append("-v") 47 | 48 | ### Apple Clang specific 49 | command.append("-integrated-as") 50 | # Quite wrong assumption - but stick to that for now 51 | is_apple_clang = system() == "Darwin" 52 | if is_apple_clang and target.architecture == "ARM64": 53 | command.extend(("-arch", "arm64")) 54 | 55 | command.extend(additional_assembler_flags) 56 | command.extend(("-o", str(output_object_file))) 57 | return command 58 | 59 | 60 | def clang_driver_is_installed() -> bool: 61 | return which(CLANG_EXECUTABLE_PATH) is not None 62 | -------------------------------------------------------------------------------- /libgofra/preprocessor/_state.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from collections.abc import Generator, Iterable, Iterator 3 | from pathlib import Path 4 | 5 | from libgofra.lexer import Token 6 | 7 | from .macros import MacrosRegistry 8 | 9 | 10 | class PreprocessorState: 11 | """State of an preprocessor stage.""" 12 | 13 | # Current file which is being processed 14 | path: Path 15 | 16 | # Lexical token streams from lexer or preprocessor itself 17 | # by default first is one from lexer and then it is extended by preprocessor to also consume next tokens from it until exhausted 18 | tokenizers: deque[Iterator[Token]] 19 | 20 | # Remember which paths was included to not include them again. 21 | already_included_paths: list[Path] 22 | 23 | # Where to additionally search for paths 24 | include_search_paths: Iterable[Path] 25 | 26 | macros: MacrosRegistry 27 | 28 | def __init__( 29 | self, 30 | path: Path, 31 | lexer: Generator[Token], 32 | include_search_paths: Iterable[Path], 33 | macros: MacrosRegistry, 34 | ) -> None: 35 | # Do not strictly resolve that path as this being lazy-toolchain and will cause to errors from lexer or previous stage to bloat preprocessor 36 | self.path = path.resolve(strict=False) 37 | 38 | self.include_search_paths = include_search_paths 39 | self.already_included_paths = [path] 40 | 41 | self.macros = macros 42 | 43 | self.tokenizers = deque((lexer,)) 44 | self.tokenizer = self.iterate_tokenizers() 45 | 46 | def iterate_tokenizers(self) -> Generator[Token]: 47 | """Consume tokens from each tokenizers until all exhausted.""" 48 | while self.tokenizers: 49 | # Consume token from current (last) tokenizer 50 | tokenizer = self.tokenizers[-1] 51 | token = next(tokenizer, None) 52 | 53 | if token: 54 | yield token 55 | continue 56 | 57 | # Current tokenizer exhausted and must be removed 58 | self.tokenizers.pop() 59 | -------------------------------------------------------------------------------- /gofra/cli/readline.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import readline 3 | import warnings 4 | from collections.abc import Callable 5 | from pathlib import Path 6 | 7 | READLINE_DEFAULT_HISTORY_LENGTH = 1000 8 | 9 | 10 | def finalize_readline(history_filepath: Path) -> None: 11 | with contextlib.suppress(FileNotFoundError): 12 | readline.write_history_file(history_filepath) 13 | 14 | 15 | def try_setup_readline(history_filepath: Path) -> None: 16 | history_filepath.touch(exist_ok=True) 17 | try: 18 | # Enable history file 19 | with contextlib.suppress(FileNotFoundError): 20 | readline.read_history_file(history_filepath) 21 | 22 | readline.set_auto_history(True) 23 | readline.set_history_length(READLINE_DEFAULT_HISTORY_LENGTH) 24 | 25 | readline.parse_and_bind(r'"\e[A": previous-history') 26 | readline.parse_and_bind(r'"\e[B": next-history') 27 | readline.parse_and_bind(r'"\e[C": forward-char') 28 | readline.parse_and_bind(r'"\e[D": backward-char') 29 | except ImportError: 30 | warnings.warn( 31 | "readline not available on this system, unable to use it!", 32 | stacklevel=1, 33 | ) 34 | 35 | 36 | def read_multiline_input( 37 | prompt_initial: str, 38 | prompt_multiline: str, 39 | raise_eof_on: Callable[[str, int], bool], 40 | stop_and_preserve_on: Callable[[str, int], bool], 41 | ) -> list[str]: 42 | lines: list[str] = [] 43 | 44 | while True: 45 | line_no = len(lines) 46 | prompt = prompt_multiline if line_no else prompt_initial 47 | try: 48 | line = input(prompt).strip() 49 | except KeyboardInterrupt: 50 | # Newline and omit after Ctrl+C 51 | print() 52 | continue 53 | 54 | if raise_eof_on(line, line_no): 55 | raise EOFError 56 | 57 | if stop_and_preserve_on(line, line_no): 58 | lines.append(line) 59 | break 60 | 61 | if line: 62 | lines.append(line) 63 | elif line_no > 0: 64 | break 65 | 66 | return lines 67 | -------------------------------------------------------------------------------- /libgofra/linker/linker.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable, MutableSequence 2 | from pathlib import Path 3 | from subprocess import PIPE, CompletedProcess, run 4 | 5 | from libgofra.linker.command_composer import ( 6 | LinkerCommandComposer, 7 | get_linker_command_composer_backend, 8 | ) 9 | from libgofra.linker.entry_point import LINKER_EXPECTED_ENTRY_POINT 10 | from libgofra.linker.output_format import LinkerOutputFormat 11 | from libgofra.linker.profile import LinkerProfile 12 | from libgofra.targets.target import Target 13 | 14 | 15 | def link_object_files( # noqa: PLR0913 16 | objects: Iterable[Path], 17 | output: Path, 18 | target: Target, 19 | output_format: LinkerOutputFormat, 20 | libraries: MutableSequence[str], 21 | additional_flags: list[str], 22 | libraries_search_paths: list[Path], 23 | profile: LinkerProfile, 24 | cache_directory: Path | None = None, 25 | executable_entry_point_symbol: str = LINKER_EXPECTED_ENTRY_POINT, 26 | *, 27 | linker_backend: LinkerCommandComposer | None = None, 28 | linker_executable: Path | None = None, 29 | ) -> CompletedProcess[bytes]: 30 | """Link given objects into another object (executable / library). 31 | 32 | Runs an new process with linker, returns it for high-level checks. 33 | """ 34 | if not linker_backend: 35 | linker_backend = get_linker_command_composer_backend(target) 36 | 37 | command = linker_backend( 38 | objects=objects, 39 | target=target, 40 | output=output, 41 | libraries=libraries, 42 | output_format=output_format, 43 | additional_flags=additional_flags, 44 | libraries_search_paths=libraries_search_paths, 45 | executable_entry_point_symbol=executable_entry_point_symbol, 46 | profile=profile, 47 | linker_executable=linker_executable, 48 | cache_directory=cache_directory, 49 | ) 50 | 51 | return run( 52 | command, 53 | check=False, 54 | capture_output=False, 55 | stdout=PIPE, 56 | shell=False, 57 | ) 58 | 59 | 60 | # TODO(@kirillzhosul): logging 61 | -------------------------------------------------------------------------------- /lib/string.gof: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // String helpers 3 | // ============================================== 4 | 5 | // TODO: Move back string length helpers, as they currently broken due to probably memory reading pollution (e.g read only single char byte instead of CPU WORD 6 | // TODO: idk how to express that but we possibly may introduce swapping variable address with an other address, or this may be fixed with named arguments 7 | // TODO: This requires legacy variant with callee-allocated buffer for int64_to_cstr 8 | #include "os/io/output.gof" 9 | 10 | 11 | // Convert int64 to string 12 | // TODO: negative numbers is not supported 13 | var int64_to_cstr_swap_buf char[20] // Max 64 bit cap 14 | var int64_to_cstr_buf char[20] // Max 64 bit cap 15 | func CStr int64_to_cstr[int num] 16 | var idx int = 0; 17 | 18 | num 0 < if 19 | // TODO: negative numbers is not supported 20 | "int64_to_cstr got negative number!\n" eprint_fatal 21 | end 22 | 23 | num 0 == if 24 | // Direct zero 25 | &int64_to_cstr_buf[0] '0' !< 26 | &int64_to_cstr_buf[1] '\0' !< 27 | &int64_to_cstr_buf return 28 | end 29 | 30 | while num 0 > do 31 | &int64_to_cstr_swap_buf[idx] 32 | num 10 % // Get last digit of an number 33 | '0' + // real `cast` to char (e.g digit + 48 => char code) 34 | !< 35 | 36 | // Shift number from right to left (drop last digit) 37 | &num num 10 / !< 38 | 39 | &idx idx 1 + !< // Increment IDX 40 | end 41 | 42 | // Swap buffers as in reverse order 43 | var a int; 44 | for b in 0..idx do 45 | // Push LTR, load RTL 46 | &a idx b - 1 - !< // Minus one so we trim last increment from previous loop: a = idx - b - 1; 47 | &int64_to_cstr_buf[b] int64_to_cstr_swap_buf[a] typecast int !< 48 | end 49 | 50 | // Null terminate before return 51 | &int64_to_cstr_buf[idx] '\0' !< 52 | 53 | &int64_to_cstr_buf return 54 | end 55 | 56 | 57 | 58 | inline func CStr int_to_cstr[int] call int64_to_cstr end // Alias as default int is 64 bits -------------------------------------------------------------------------------- /libgofra/codegen/sections/macho.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from dataclasses import dataclass 3 | from typing import Literal 4 | 5 | 6 | @dataclass(frozen=True, slots=True) 7 | class MachOSection: 8 | """Specification of *directive* of section/segment for Mach-O object format.""" 9 | 10 | segment: Literal["__TEXT", "__DATA"] 11 | section: str 12 | attributes: Sequence[ 13 | Literal[ 14 | # Section types (mutually exclusive - choose ONE): 15 | "regular", 16 | "zerofill", 17 | "cstring_literals", 18 | # Attributes (only valid with 'regular' type): 19 | "pure_instructions", 20 | ] 21 | ] 22 | 23 | def __post_init__(self) -> None: 24 | if "zerofill" in self.attributes or "cstring_literals" in self.attributes: 25 | assert len(self.attributes) == 1 26 | return 27 | 28 | assert all( 29 | x in {"regular", "pure_instructions", "strip_static_syms"} 30 | for x in self.attributes 31 | ), f"Prohibited attribute sequence for {self.section}" 32 | 33 | def __str__(self) -> str: 34 | """Get section directive that can be used in assembler section.""" 35 | return ",".join((self.segment, self.section, *self.attributes)) 36 | 37 | 38 | MACHO_SECTION_BSS = MachOSection( 39 | segment="__DATA", 40 | section="__bss", 41 | attributes=("zerofill",), 42 | ) # Uninitialized variables (same as data but different within initialization at runtime by kernel) 43 | 44 | MACHO_SECTION_DATA = MachOSection( 45 | segment="__DATA", 46 | section="__data", 47 | attributes=(), 48 | ) # Variables that is read-write and initialized (has initial value) 49 | 50 | MACHO_SECTION_STRINGS = MachOSection( 51 | segment="__TEXT", 52 | section="__cstring", 53 | attributes=("cstring_literals",), 54 | ) # Pure C-strings, read-only (string text only) 55 | 56 | MACHO_SECTION_INSTRUCTIONS = MachOSection( 57 | segment="__TEXT", 58 | section="__text", 59 | attributes=("regular", "pure_instructions"), 60 | ) # Only instructions here, code goes here 61 | -------------------------------------------------------------------------------- /scripts/macos_syscall_extractor.py: -------------------------------------------------------------------------------- 1 | """Extract system call numbers from OS headers.""" 2 | 3 | # TODO(@stepanzubkov, @kirillzhosul): Refactor for Linux also 4 | 5 | import subprocess 6 | 7 | SYSCALL_MACRO_PREFIX = "SYS_" 8 | SYSTEM_INCLUDES = [ 9 | "-isystem", 10 | "/usr/include", 11 | "-isystem", 12 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include", 13 | ] 14 | 15 | 16 | def extract_raw_kernel_syscall_macro_definitions( 17 | *, 18 | include_max_syscall: bool = False, 19 | ) -> dict[str, int]: 20 | """Extract all syscall numbers defined inside macros (e.g C/C++ #define) that is included in c stdlib from kernel. 21 | 22 | They are used for making syscalls from C/C++ but we extracting them for fresh real-time mapping of all syscalls 23 | 24 | Implementation: 25 | Calls to C compiler to preprocess an file with include of syscall header 26 | Then extracts all definitions that starts with SYS_* (e.g all syscall numbers) and returns to the caller 27 | """ 28 | cmd = ["cc", "-E", "-dM", "-", *SYSTEM_INCLUDES] 29 | source = b"#include " 30 | 31 | process = subprocess.run( 32 | cmd, 33 | input=source, 34 | stdout=subprocess.PIPE, 35 | check=True, 36 | ) 37 | 38 | stdout = process.stdout.decode() 39 | 40 | syscalls: dict[str, int] = {} 41 | it = ( 42 | line 43 | for line in stdout.split("\n") 44 | if line.startswith(f"#define {SYSCALL_MACRO_PREFIX}") 45 | ) 46 | for line in it: 47 | _, name, value, *_ = line.split() 48 | if "__" in name: 49 | continue 50 | name = name.removeprefix(SYSCALL_MACRO_PREFIX) 51 | syscalls[name] = int(value) 52 | 53 | if not include_max_syscall: 54 | syscalls.pop("MAXSYSCALL") 55 | return dict(sorted(syscalls.items(), key=lambda x: x[1])) 56 | 57 | 58 | if __name__ == "__main__": 59 | raw_syscalls = extract_raw_kernel_syscall_macro_definitions() 60 | for syscall_name, syscall_number in raw_syscalls.items(): 61 | print(syscall_name, "=", syscall_number) 62 | -------------------------------------------------------------------------------- /libgofra/lexer/literals/character.py: -------------------------------------------------------------------------------- 1 | """Lexer support for character literals.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from libgofra.lexer.errors import ( 8 | EmptyCharacterLiteralError, 9 | ExcessiveCharacterLengthError, 10 | UnclosedCharacterQuoteError, 11 | ) 12 | from libgofra.lexer.helpers import ( 13 | find_quoted_literal_end, 14 | find_word_start, 15 | unescape_text_literal, 16 | ) 17 | from libgofra.lexer.tokens import Token, TokenType 18 | 19 | if TYPE_CHECKING: 20 | from libgofra.lexer._state import LexerState 21 | 22 | 23 | CHARACTER_QUOTE = "'" 24 | 25 | 26 | def tokenize_character_literal(state: LexerState) -> Token: 27 | """Tokenize character quote at cursor into character literal token or raise error if invalid character.""" 28 | assert state.line and state.line[state.col] == CHARACTER_QUOTE, ( # noqa: PT018 29 | f"{tokenize_character_literal.__name__} must be called when cursor is at open character quote" 30 | ) 31 | 32 | location = state.current_location() 33 | state.col += len(CHARACTER_QUOTE) 34 | 35 | close_quote_col = _find_character_literal_end(state) - 1 36 | if close_quote_col < 0: 37 | raise UnclosedCharacterQuoteError(open_quote_at=location) 38 | 39 | # Preserve literal as 'X' and unescaped version 40 | # TODO(@kirillzhosul): Character literal escape messed when trying to escape last quote 41 | char_literal = state.line[state.col - 1 : close_quote_col + 1] 42 | char = unescape_text_literal(char_literal.strip(CHARACTER_QUOTE)) 43 | 44 | if len(char) == 0: 45 | raise EmptyCharacterLiteralError(open_quote_at=location) 46 | if len(char) >= 2: 47 | raise ExcessiveCharacterLengthError(location, len(char)) 48 | 49 | # Advance to next word 50 | state.col = find_word_start(state.line, close_quote_col + 1) 51 | return Token( 52 | type=TokenType.CHARACTER, 53 | text=char_literal, 54 | value=ord(char), # Char-code 55 | location=location, 56 | ) 57 | 58 | 59 | def _find_character_literal_end(state: LexerState) -> int: 60 | return find_quoted_literal_end(state.line, state.col, quote=CHARACTER_QUOTE) 61 | -------------------------------------------------------------------------------- /docs/advanced/ffi.md: -------------------------------------------------------------------------------- 1 | # FFI (Foreign Function Interface) 2 | 3 | Read about FFI at [Wikipedia](https://en.wikipedia.org/wiki/Foreign_function_interface) 4 | 5 | This documentation page is written for `AARCH64_MacOS` target and may be irrelevant for some underneath implementations on different targets, like FFI naming conventions on different assemblers 6 | 7 | --- 8 | 9 | Gofra is capable of calling functions that written in other languages and externally defined, for example using libraries written for/in `C` or other language, or using system libraries. 10 | 11 | To do that assembler after code generation at linker stage should be acknowledged of external libraries (via CLI linker flags) and Gofra source code should specify which functions being external for FFI. 12 | 13 | 14 | # `extern` marker 15 | 16 | Functions marked with `extern` keyword/marker will be treated as externally defined via FFI for example external function `puts` from `libc` is declared as: 17 | ```gofra 18 | extern func int _puts[*char[]] 19 | ``` 20 | 21 | This is written according to `libc` library `C` interface: 22 | ```C 23 | int puts(const char *s){ ... } 24 | ``` 25 | 26 | External functions in Gofra **CANNOT** have any tokens inside otherwise compiler will throw an error 27 | 28 | # Type contracts for FFI 29 | 30 | To specify which data is expected for and from external function you specify type contract for that functions (like default functions in Gofra) 31 | 32 | Every argument will be treated by compiler and will be passed to that function, as well as external function return data will be pushed onto stack for you 33 | 34 | # Variadic arguments (type contracts) 35 | 36 | For now, there is no way to pass variadic arguments to external function 37 | (Hack or workaround for that is only to create non-variadic wrappers with different number of arguments to pass to underlying external function which type contract is expanded for max required amount of arguments) 38 | 39 | # Linker stage 40 | 41 | For letting linker know about that dependencies (Libraries with FFI), you can pass `-l` flag(s) to the compiler 42 | 43 | For example linking with `raylib` and using their functions: 44 | `-lraylib` (`-L/opt/homebrew/lib` if search directory was not found with pkgconfig (`--no-pkg-config`)) 45 | -------------------------------------------------------------------------------- /libgofra/optimizer/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses 4 | from dataclasses import dataclass 5 | from typing import Literal, assert_never 6 | 7 | type OPTIMIZER_LEVEL = Literal[0, 1] 8 | 9 | 10 | @dataclass 11 | class OptimizerConfig: 12 | """Configuration for optimizer passes.""" 13 | 14 | level: OPTIMIZER_LEVEL 15 | 16 | # Feature flags 17 | do_function_inlining: bool 18 | do_dead_code_elimination: bool 19 | do_constant_folding: bool 20 | do_algebraic_simplification: bool 21 | do_strength_reduction: bool 22 | 23 | # Fine tuning optimizations 24 | function_inlining_max_operators: int = 10 25 | function_inlining_max_iterations: int = 128 26 | dead_code_elimination_max_iterations: int = 128 27 | 28 | 29 | def build_default_optimizer_config_from_level( 30 | level: OPTIMIZER_LEVEL, 31 | ) -> OptimizerConfig: 32 | """Construct optimizer config with default settings inferred from level.""" 33 | if level == 0: 34 | return OptimizerConfig( 35 | level=level, 36 | do_function_inlining=False, 37 | do_dead_code_elimination=False, 38 | do_constant_folding=False, 39 | do_algebraic_simplification=False, 40 | do_strength_reduction=False, 41 | ) 42 | if level == 1: 43 | return OptimizerConfig( 44 | level=level, 45 | do_dead_code_elimination=True, 46 | do_function_inlining=True, 47 | # TODO(@kirillzhosul): Those optimizations are pending to be implemented. 48 | do_constant_folding=False, 49 | do_algebraic_simplification=False, 50 | do_strength_reduction=False, 51 | ) 52 | 53 | assert_never(level) 54 | 55 | 56 | def merge_into_optimizer_config( 57 | config: OptimizerConfig, 58 | from_object: object, 59 | *, 60 | prefix: str = "", 61 | ) -> OptimizerConfig: 62 | for field in dataclasses.fields(OptimizerConfig): 63 | from_name = prefix + "_" + field.name 64 | if hasattr(from_object, from_name): 65 | arg_value = getattr(from_object, from_name) 66 | if arg_value is not None: 67 | setattr(config, field.name, arg_value) 68 | return config 69 | -------------------------------------------------------------------------------- /libgofra/optimizer/pipeline.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, MutableSequence 2 | from functools import partial 3 | 4 | from libgofra.hir.module import Module 5 | from libgofra.optimizer.config import OptimizerConfig 6 | from libgofra.optimizer.strategies.dead_code_elimination import ( 7 | optimize_dead_code_elimination, 8 | ) 9 | from libgofra.optimizer.strategies.function_inlining import optimize_function_inlining 10 | 11 | type OPTIMIZER_PASS_T = Callable[[Module], None] 12 | type OPTIMIZER_PIPELINE_T = MutableSequence[tuple[OPTIMIZER_PASS_T, str]] 13 | 14 | 15 | def create_optimizer_pipeline( 16 | config: OptimizerConfig, 17 | ) -> OPTIMIZER_PIPELINE_T: 18 | """Build an optimizer `pipeline` from given config. 19 | 20 | You must apply each optimization pass for program context, they will mutate it. 21 | Each pass from pipeline contains 22 | """ 23 | assert not config.do_algebraic_simplification, "TODO: do_algebraic_simplification!" 24 | assert not config.do_constant_folding, "TODO: do_constant_folding!" 25 | assert not config.do_strength_reduction, "TODO: do_strength_reduction!" 26 | 27 | pipeline: OPTIMIZER_PIPELINE_T = [] 28 | 29 | if config.do_function_inlining: 30 | name = "Function inlining" 31 | pipe = _pipelined_function_inlining( 32 | max_operators=config.function_inlining_max_operators, 33 | max_iterations=config.function_inlining_max_iterations, 34 | ) 35 | pipeline.append((pipe, name)) 36 | 37 | if config.do_dead_code_elimination: 38 | name = "DCE (dead-code-elimination)" 39 | pipe = _pipelined_dead_code_elimination( 40 | max_iterations=config.dead_code_elimination_max_iterations, 41 | ) 42 | pipeline.append((pipe, name)) 43 | 44 | return pipeline 45 | 46 | 47 | def _pipelined_dead_code_elimination(max_iterations: int) -> OPTIMIZER_PASS_T: 48 | return partial(optimize_dead_code_elimination, max_iterations=max_iterations) 49 | 50 | 51 | def _pipelined_function_inlining( 52 | max_operators: int, 53 | max_iterations: int, 54 | ) -> OPTIMIZER_PASS_T: 55 | return partial( 56 | optimize_function_inlining, 57 | max_operators=max_operators, 58 | max_iterations=max_iterations, 59 | ) 60 | -------------------------------------------------------------------------------- /libgofra/preprocessor/conditions/__init__.py: -------------------------------------------------------------------------------- 1 | """Conditions system for definitions in preprocessor.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from libgofra.lexer.keywords import PreprocessorKeyword 8 | from libgofra.lexer.tokens import Token, TokenType 9 | 10 | from .exceptions import ( 11 | PreprocessorConditionalConsumeUntilEndifContextSwitchError, 12 | PreprocessorConditionalNoMacroNameError, 13 | ) 14 | 15 | if TYPE_CHECKING: 16 | from libgofra.preprocessor._state import PreprocessorState 17 | from libgofra.preprocessor.macros.macro import Macro 18 | 19 | 20 | def resolve_conditional_block_from_token( 21 | token: Token, 22 | state: PreprocessorState, 23 | ) -> None: 24 | macro = _consume_macro_from_token(token, state) 25 | match token: 26 | case Token(type=TokenType.KEYWORD, value=PreprocessorKeyword.IF_DEFINED): 27 | if not macro: 28 | _consume_until_endif(token, state) 29 | case Token(type=TokenType.KEYWORD, value=PreprocessorKeyword.IF_NOT_DEFINED): 30 | if macro: 31 | _consume_until_endif(token, state) 32 | case _: 33 | msg = "Expected resolve conditional block to receive an known preprocessor keyword token" 34 | raise AssertionError(msg) 35 | 36 | 37 | def _consume_until_endif(from_token: Token, state: PreprocessorState) -> None: 38 | while token := next(state.tokenizer, None): 39 | if token.type != TokenType.KEYWORD: 40 | continue 41 | if token.value == PreprocessorKeyword.END_IF: 42 | return 43 | if token.location.filepath != from_token.location.filepath: 44 | raise PreprocessorConditionalConsumeUntilEndifContextSwitchError( 45 | conditional_token=from_token, 46 | ) 47 | 48 | 49 | def _consume_macro_from_token(token: Token, state: PreprocessorState) -> Macro | None: 50 | macro_name = next(state.tokenizer, None) 51 | if not macro_name: 52 | raise PreprocessorConditionalNoMacroNameError(conditional_token=token) 53 | if macro_name.type != TokenType.IDENTIFIER: 54 | raise ValueError(macro_name) 55 | assert isinstance(macro_name.value, str) 56 | return state.macros.get(macro_name.value) 57 | -------------------------------------------------------------------------------- /libgofra/lexer/tests/test_character_literals.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from libgofra.lexer._state import LexerState 4 | from libgofra.lexer.errors.empty_character_literal import EmptyCharacterLiteralError 5 | from libgofra.lexer.errors.excessive_character_length import ( 6 | ExcessiveCharacterLengthError, 7 | ) 8 | from libgofra.lexer.errors.unclosed_character_quote import UnclosedCharacterQuoteError 9 | from libgofra.lexer.literals.character import tokenize_character_literal 10 | from libgofra.lexer.tokens import Token, TokenType 11 | 12 | 13 | def test_character_literal_tokenize_empty_state() -> None: 14 | state = LexerState(path="toolchain") 15 | with pytest.raises(AssertionError): 16 | tokenize_character_literal(state) 17 | 18 | 19 | def test_character_literal_tokenize_valid() -> None: 20 | state = LexerState(path="toolchain") 21 | state.set_line(0, "'a'") 22 | _assert_is_character_token(tokenize_character_literal(state)) 23 | 24 | 25 | def test_character_literal_tokenize_unclosed() -> None: 26 | state = LexerState(path="toolchain") 27 | state.set_line(0, "'a") 28 | with pytest.raises(UnclosedCharacterQuoteError): 29 | tokenize_character_literal(state) 30 | 31 | 32 | def test_character_literal_tokenize_excessive() -> None: 33 | state = LexerState(path="toolchain") 34 | state.set_line(0, "'abc'") 35 | with pytest.raises(ExcessiveCharacterLengthError): 36 | tokenize_character_literal(state) 37 | 38 | 39 | def test_character_literal_tokenize_empty() -> None: 40 | state = LexerState(path="toolchain") 41 | state.set_line(0, "''") 42 | with pytest.raises(EmptyCharacterLiteralError): 43 | tokenize_character_literal(state) 44 | 45 | 46 | def test_character_literal_tokenize_escaped() -> None: 47 | state = LexerState(path="toolchain") 48 | state.set_line(0, r"'\n'") 49 | _assert_is_character_token(tokenize_character_literal(state)) 50 | 51 | 52 | @pytest.mark.skip("Does not work") 53 | def test_character_literal_tokenize_escaped_quote() -> None: 54 | state = LexerState(path="toolchain") 55 | state.set_line(0, r"'\\'") 56 | _assert_is_character_token(tokenize_character_literal(state)) 57 | 58 | 59 | def _assert_is_character_token(t: Token) -> None: 60 | assert t.type == TokenType.CHARACTER 61 | -------------------------------------------------------------------------------- /libgofra/preprocessor/macros/macro.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import deque 4 | from dataclasses import dataclass, field 5 | from typing import TYPE_CHECKING 6 | 7 | from libgofra.lexer import Token 8 | 9 | if TYPE_CHECKING: 10 | from collections.abc import MutableSequence 11 | 12 | from libgofra.lexer.tokens import TokenLocation 13 | 14 | 15 | @dataclass(frozen=True) 16 | class Macro: 17 | """Preprocessor macro definition for text substitution and conditional compilation. 18 | 19 | Macros are named containers of sequence of tokens (e.g raw text) to be expanded 20 | when the macro name invocation is encountered during preprocessing. 21 | 22 | They also used and desired to be used in preprocessor conditional blocks 23 | (e.g `#if`, `#ifdef` for conditional compilation) 24 | 25 | Language workflow: 26 | Preprocessor encounter macro definition like: 27 | `#define VALUE 1024` 28 | 29 | It consumes that whole block until `EOL` (end-of-line) token (macro definitions are line-dependant) 30 | Next time when preprocessor encounter an name of that token in tokens, e.g: 31 | `{code} VALUE {code}` 32 | It will consume that invocation and expand that tokens which that macro contains, e.g: 33 | `{code} 1024 {code}` 34 | 35 | That stage is related to preprocessor so all definition directives will be eliminated before parser stage. 36 | 37 | Command-Line-Interface (CLI) definitions: 38 | Definitions (macros) may be propagated from the CLI via `-D` flag, e.g: `-DMACRO_NAME` or `-DMACRO_NAME=128` 39 | They will be treated as same as file-contained definitions would be. 40 | 41 | Read more: 42 | Text substitution macros: https://en.wikipedia.org/wiki/Macro_(computer_science)#Text-substitution_macros 43 | """ 44 | 45 | # Where is that definitions begins (an reference to `#define` token) 46 | # There is possibility that definition is comes from CLI or toolchain 47 | location: TokenLocation 48 | 49 | # Definition block name 50 | # stored here as possible future reference, mostly should be unused 51 | name: str 52 | 53 | # Actual macro container, contains tokens which that macro would expand into 54 | tokens: MutableSequence[Token] = field(default_factory=deque[Token]) 55 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Gofra [beta] 2 | 3 | **A Stack-based compiled programming language.** 4 | 5 | **The project is made not for competing with other languages, rather it is another incomplete language almost same like others.** 6 | 7 | ## Overview 8 | Gofra is a **concatenative** (stack-based) programming language that compiles to native code. 9 | Programs are written using [Reverse Polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation), where operations follow their operands (e.g `2 + 2` is `2 2 +`). 10 | 11 | ## Quick start 12 | 13 | Here's a simple **"Hello, World!"** example: 14 | ```gofra 15 | include "std.gof" 16 | 17 | func void main 18 | "Hello, World!" println 19 | end 20 | ``` 21 | 22 | ## Features 23 | - *Native* - Compiles to native machine code 24 | - *C*-like - Close to C, but has a few more high-level abstraction (`for in`, `type generics`) 25 | - *C FFI* - Seamless integration with **C**-FFI libraries 26 | - *Low-level* - Write unsafe, low-level code with direct memory access 27 | - *Type System* - Validate types at compile time, has compile-time warnings (Partial generics supports, typechecker) 28 | - *Library* - By default has support for `math`, `random`, `network` and other libraries 29 | 30 | ## Showcase 31 | - Pong Game (`examples/03_pong.gof`) 32 | - Simple HTTP server (`examples/04_http_server.gof`) 33 | 34 | ## Milestones 35 | 36 | ## Platform support 37 | Gofra currently supports native compilation (no cross-compilation yet). You must compile on the same platform as your target. 38 | 39 | - Full: **AArch64** macOS (Darwin) 40 | - Partial, buggy: **x86_64** (Linux) 41 | 42 | (Windows **x86_64** is must be supported soon, requires contributors) 43 | 44 | ## Pre requirements 45 | 46 | Before installing Gofra, ensure you have the following tools available system-wide: 47 | 48 | - [Python >3.12.x](https://www.python.org) 49 | - GNU/Mach-O Linker (ld) - For linking compiled objects 50 | - Assembler (as) - Typically included with Clang LLVM compiler 51 | 52 | ## Installation 53 | 54 | **For full installation steps, please visit [Installation](./installation.md) page.** 55 | 56 | [Gofra](https://github.com/kirillzhosul/gofra) is distributed as single Python-based toolchain. To install: 57 | 58 | (Step 1): Install toolchain 59 | ```bash 60 | pip install gofra 61 | ``` 62 | (Step 2): Verify Installation 63 | ```bash 64 | gofra --help 65 | ``` -------------------------------------------------------------------------------- /libgofra/parser/runtime_oob_check.py: -------------------------------------------------------------------------------- 1 | from libgofra.feature_flags import ( 2 | FEATURE_RUNTIME_ARRAY_OOB_CHECKS, 3 | ) 4 | from libgofra.hir.operator import OperatorType 5 | from libgofra.hir.variable import ( 6 | Variable, 7 | ) 8 | from libgofra.lexer.tokens import Token, TokenType 9 | from libgofra.parser._context import ParserContext 10 | from libgofra.types.primitive.integers import I64Type 11 | 12 | 13 | def emit_runtime_hir_oob_check( 14 | context: ParserContext, 15 | token: Token, 16 | index_var: Variable[I64Type], 17 | elements_const: int, 18 | ) -> None: 19 | """Push operators to perform OOB check at runtime. 20 | 21 | TODO(@kirillzhosul): Must be reworked into runtime lib?: 22 | Possibly introduce runtime include library, should requires via something like `require_runtime_function` 23 | """ 24 | assert FEATURE_RUNTIME_ARRAY_OOB_CHECKS 25 | if "eprint_fatal" not in context.functions: 26 | msg = "Cannot do FEATURE_RUNTIME_ARRAY_OOB_CHECKS unless stdlib (`eprint_fatal`) is available for panic" 27 | raise ValueError(msg) 28 | panic_msg = "Runtime OOB error: tried to access array with out-of-bounds index, step into debugger for help!\\n" 29 | context.push_new_operator( 30 | OperatorType.PUSH_VARIABLE_VALUE, 31 | token, 32 | operand=index_var.name, 33 | ) 34 | context.push_new_operator( 35 | OperatorType.PUSH_INTEGER, 36 | operand=elements_const, 37 | token=token, 38 | ) 39 | context.push_new_operator(OperatorType.COMPARE_GREATER_EQUALS, token) 40 | context.push_new_operator(OperatorType.CONDITIONAL_IF, token, is_contextual=True) 41 | if True: 42 | context.push_new_operator( 43 | OperatorType.PUSH_STRING, 44 | operand=panic_msg, 45 | token=Token( 46 | type=TokenType.STRING, 47 | text=f'"{panic_msg}"', 48 | value=panic_msg, 49 | location=token.location, 50 | ), 51 | ) 52 | context.push_new_operator( 53 | OperatorType.FUNCTION_CALL, 54 | token=token, 55 | operand="eprint_fatal", 56 | ) 57 | context.push_new_operator(OperatorType.CONDITIONAL_END, token=token) 58 | _, if_op, _ = context.pop_context_stack() 59 | if_op.jumps_to_operator_idx = context.current_operator - 1 60 | --------------------------------------------------------------------------------