diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..17d6464 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_nix \ No newline at end of file diff --git a/config.py b/config.py index 37e6f52..6aebdea 100644 --- a/config.py +++ b/config.py @@ -30,6 +30,9 @@ class Config: "stdout" : "File to output stdout of complier and program", "input" : "Stdin for program", "error" : "Stderr for program", + "link_with_libc" : "Statically link with libc", + # "library_path" : "add library path 'dir'", + # "link_with" : "link with dynamic or static library 'lib'" } BOOL_OPTIONS: Dict[str, Tuple[List[str], bool]] = { @@ -37,9 +40,10 @@ class Config: "dump" : (["-d", "-dump"], False), "dump_tokens" : (["-dt", "-dump_tokens"], False), "dump_tc" : (["-dtc", "-dump_tc"], False), + "link_with_libc": (["-lc", "--libc"], False), } - REGULAR_OPTIONS: Dict[str, List[str]] = { + REGULAR_OPTIONS: Dict[str, Tuple[List[str], bool]] = { "out" : (["-o", "--out"], None), "target" : (["-t", "--target"], "fasm_x86_64_linux"), "dump_proc" : (["-dp", "--dump_proc"], None), @@ -47,6 +51,12 @@ class Config: "input" : (["-i", "--input"], None), "error" : (["-e", "--error"], None), } + + ARRAY_OPTIONS: Dict[str, Tuple[List[str], bool]] = { + # "library_path" : ((["-L", "--library-path"]), None), + # "link_with" : ((["-l", "--link-with"]), None), + } + CONFIG_REGULAR_OPTIONS: List[str] = ["out", "target"] CONFIG_BOOL_OPTIONS: Dict[str, bool] = { @@ -102,6 +112,11 @@ def setup_args_parser(self) -> argparse.ArgumentParser: *args[0], default=None, dest=name, help=self.DESCRIPTIONS[name] ) + for name, args in self.ARRAY_OPTIONS.items(): + args_parser.add_argument( + *args[0], default=None, dest=name, help=self.DESCRIPTIONS[name], action='append' + ) + return args_parser def _validate_target(self): @@ -152,6 +167,17 @@ def define_properties(self): ) ) + for name in self.ARRAY_OPTIONS: + setattr( + self.__class__, name, + property( + fget=lambda self, name=name: self.config.get( + name, getattr(self.args, name) + if getattr(self.args, name) is not None + else self.REGULAR_OPTIONS[name][1]) + ) + ) + for name, default in {**self.CONFIG_BOOL_OPTIONS, **self.CONFIG_INT_OPTIONS}.items(): setattr( self.__class__, name, diff --git a/cont.py b/cont.py index 9ac5bfb..4ec6cc0 100644 --- a/cont.py +++ b/cont.py @@ -15,6 +15,7 @@ def main(lsp_mode: bool = False): if the function is used in code and not by calling the script from command line. """ config = Config(sys.argv, lsp_mode=lsp_mode) + State.config = config file_name = os.path.splitext(config.program)[0] diff --git a/generating/fasm_x86_64_linux.py b/generating/fasm_x86_64_linux.py index 4bd4cb7..8b4df44 100644 --- a/generating/fasm_x86_64_linux.py +++ b/generating/fasm_x86_64_linux.py @@ -36,6 +36,11 @@ def compile_ops_fasm_x86_64_linux(ops: List[Op]): f.write(generate_fasm_x86_64_linux(ops)) subprocess.run(["fasm", f"{out}.asm"], stdin=sys.stdin, stderr=sys.stderr) + + linker_args = ["ld", f"{out}.o", "-o", f"{out}"] + if State.config.link_with_libc: + linker_args += ["-lc", "--static"] + subprocess.run(linker_args, stdin=sys.stdin, stderr=sys.stderr) os.chmod( out, os.stat(out).st_mode | stat.S_IEXEC ) # Give execution permission to the file @@ -113,9 +118,10 @@ def compile_ops_fasm_x86_64_linux(ops: List[Op]): def generate_fasm_x86_64_linux(ops: List[Op]) -> str: """Generates a string of fasm assembly for the program from the list of operations `ops`.""" buf = ( - "format ELF64 executable 3\n" - "segment readable executable\n" - "entry _start\n" + "format ELF64\n" + f"{generate_imports()}" + "section '.text' executable\n" + "public _start\n" f"{INDEX_ERROR_CODE if State.config.re_IOR else ''}" f"{NULL_POINTER_CODE if State.config.re_NPD else ''}" "_start:\n" @@ -135,7 +141,7 @@ def generate_fasm_x86_64_linux(ops: List[Op]) -> str: "mov rax, 60\n" "xor rdi, rdi\n" "syscall\n" - "segment readable writeable\n" + "section '.data' writeable\n" f"{ior_code if State.config.re_IOR else ''}\n" f"{npd_code if State.config.re_NPD else ''}\n" f"{generate_fasm_types()}\n" @@ -160,6 +166,15 @@ def generate_fasm_x86_64_linux(ops: List[Op]) -> str: return buf +def generate_imports() -> str: + buf = "" + for (proc_name, _) in State.imported_procs: + buf += ( + f"extrn {proc_name}\n" + ) + + return buf + def generate_fasm_types() -> str: """ Generates a string of assembly, which if put into the .data segment, @@ -419,6 +434,19 @@ def generate_op_fasm_x86_64_linux(op: Op) -> str: "push rax\n" ) elif op.type == OpType.CALL: + buf = "" + + if op.operand.is_imported: + calling_convention = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"] + in_stack = op.operand.in_stack + while len(in_stack) != 0 and len(calling_convention) != 0: + buf += f"pop {calling_convention[0]}\n" + calling_convention.pop(0) + in_stack.pop(0) + + buf += f"call {op.operand.name}\npush rax\n" + return comment + buf + return comment + f"call addr_{op.operand.ip}\n" elif op.type == OpType.TYPED_LOAD: cont_assert(not isinstance(op.operand, Struct), "Bug in parsing of structure types") diff --git a/parsing/parsing.py b/parsing/parsing.py index 5da2c64..28e76da 100644 --- a/parsing/parsing.py +++ b/parsing/parsing.py @@ -991,10 +991,14 @@ def parse_token(token: str, ops: List[Op]) -> Union[Op, List[Op]]: return Op(OpType.CALL_ADDR, None) elif token == "#import": - assert State.config.target == "wat64", "Current target does not support imports" - + assert State.config.target == "wat64" or State.config.target == "fasm_x86_64_linux", "Current target does not support imports" + + path = "" name, name_loc = safe_next_token("Expected a function name") - path, _ = safe_next_token("Expected a path") + + if State.config.target == "wat64": + path, _ = safe_next_token("Expected a path") + State.check_name((name, name_loc)) if ";" not in name: in_types, out_types, _ = parse_signature((name, name_loc), {}, ";") diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..65171b1 --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + gcc14 + python312Full + nodejs_23 + wabt + fasm-bin + musl + python312Packages.pytest + ]; +} \ No newline at end of file diff --git a/std/core.cn b/std/core.cn index 5b02081..c950969 100644 --- a/std/core.cn +++ b/std/core.cn @@ -1,5 +1,8 @@ include platform.cn include stack.cn +#if platform Platform.fasm_x86_64_linux ==; + include ffi.cn +#endif proc / int int -> int: div drop end proc % int int -> int: div swap drop end diff --git a/std/ffi.cn b/std/ffi.cn new file mode 100644 index 0000000..f129ab3 --- /dev/null +++ b/std/ffi.cn @@ -0,0 +1,2 @@ +typealias c_int int +typealias c_void void diff --git a/test.js b/test.js index 4d205c9..3b7857a 100644 --- a/test.js +++ b/test.js @@ -6,6 +6,7 @@ function ContExitException(message) { error.name = "ContExitException"; return error; } + ContExitException.prototype = Object.create(Error.prototype); function bnToBuf(bn) { diff --git a/test.py b/test.py index b8b79e6..482d37d 100644 --- a/test.py +++ b/test.py @@ -37,7 +37,7 @@ def test(test_name): subprocess.run( [ - "python", "cont.py", f"tests/temp/code_{test_name}.cn", + "python", "cont.py", "-lc", f"tests/temp/code_{test_name}.cn", "-t", "fasm_x86_64_linux", "-i", f"tests/temp/stdin_{test_name}", "-e", f"tests/results/{test_name}_stderr", @@ -68,6 +68,9 @@ def test(test_name): else: @pytest.mark.parametrize("test_name", tests) def test_node_wat64(test_name): + if test_name == "extern": + return + with open(f"tests/{test_name}", "r") as f: test = f.read() diff --git a/tests/extern b/tests/extern new file mode 100644 index 0000000..9bf0e1d --- /dev/null +++ b/tests/extern @@ -0,0 +1,7 @@ +#import puts ptr -> int; +#import exit int; + +"Hello, World!\0" puts drop +0 exit +: +Hello, World! diff --git a/type_checking/types.py b/type_checking/types.py index 41b2a26..4d1ef2a 100644 --- a/type_checking/types.py +++ b/type_checking/types.py @@ -184,6 +184,20 @@ def __hash__(self) -> int: return hash(self.text_repr()) +class Void(Type): + """ + Void type. Provided for C FFI Purposes. + """ + def __eq__(self, other) -> bool: + return true + + def text_repr(self) -> str: + return f"void" + + def __hash__(self) -> int: + return hash(self.text_repr()) + + def type_to_str(_type: Type) -> str: """ Converts cont type object to a human-readable string @@ -260,6 +274,8 @@ def parse_type( result = Int() elif name == "ptr": result = Ptr() + elif name == "void": + result = Void() elif name == "addr": assert end is None or end not in og_name, "Expected procedure name" try: