diff --git a/src/template.py b/src/template.py index e54a2f5..763e13d 100644 --- a/src/template.py +++ b/src/template.py @@ -5,21 +5,77 @@ Creation of templates for Exercitium problems import ast import re -import shutil from argparse import ArgumentParser +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class Example: - input_value: object + input_values: tuple[object, ...] expected_value: object def load_instructions(path: Path) -> str: return path.read_text(encoding="utf-8").strip() +def split_top_level_values(text: str) -> list[str]: + parts: list[str] = [] + current: list[str] = [] + depth = 0 + quote_char: str | None = None + escaped = False + + for char in text.strip(): + if quote_char is not None: + current.append(char) + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == quote_char: + quote_char = None + continue + + if char in {"'", '"'}: + quote_char = char + current.append(char) + continue + + if char in "([{": + depth += 1 + current.append(char) + continue + + if char in ")]}": + depth -= 1 + current.append(char) + continue + + if char.isspace() and depth == 0: + if current: + parts.append("".join(current)) + current = [] + continue + + current.append(char) + + if quote_char is not None or depth != 0: + raise ValueError(f"Cannot parse input values: {text}") + + if current: + parts.append("".join(current)) + + return parts + + +def parse_input_values(text: str) -> tuple[object, ...]: + parts = split_top_level_values(text) + if not parts: + raise ValueError("No input values found") + + return tuple(ast.literal_eval(part) for part in parts) def parse_instructions(text: str) -> tuple[str, list[Example]]: examples: list[Example] = [] @@ -43,7 +99,7 @@ def parse_instructions(text: str) -> tuple[str, list[Example]]: examples.append( Example( - input_value=ast.literal_eval(match.group(2).strip()), + input_values=parse_input_values(match.group(2).strip()), expected_value=ast.literal_eval(right.strip()), ) ) @@ -53,6 +109,8 @@ def parse_instructions(text: str) -> tuple[str, list[Example]]: return function_name, examples +def build_names(prefix: str, count: int) -> list[str]: + return [f"{prefix}_{index}" for index in range(1, count + 1)] def infer_python_type(value: object) -> str: if isinstance(value, bool): @@ -64,6 +122,9 @@ def infer_python_type(value: object) -> str: if isinstance(value, list): inner_type = infer_python_type(value[0]) if value else "object" return f"list[{inner_type}]" + if isinstance(value, tuple): + inner_types = ", ".join(infer_python_type(item) for item in value) + return f"tuple[{inner_types}]" return "object" @@ -90,6 +151,9 @@ def infer_rust_type(value: object) -> str: if isinstance(value, list): inner_type = infer_rust_type(value[0]) if value else "i32" return f"Vec<{inner_type}>" + if isinstance(value, tuple): + inner_types = ", ".join(infer_rust_type(item) for item in value) + return f"({inner_types})" raise TypeError(f"Unsupported Rust type: {value!r}") @@ -109,6 +173,8 @@ def render_julia(value: object) -> str: return f'"{value}"' if isinstance(value, list): return "[" + ", ".join(render_julia(item) for item in value) + "]" + if isinstance(value, tuple): + return "(" + ", ".join(render_julia(item) for item in value) + ")" return str(value) @@ -129,51 +195,167 @@ def render_rust(value: object) -> str: return f'"{value}"' if isinstance(value, list): return "vec![" + ", ".join(render_rust(item) for item in value) + "]" + if isinstance(value, tuple): + return "(" + ", ".join(render_rust(item) for item in value) + ")" return str(value) def build_python_checks(function_name: str, examples: list[Example]) -> str: - return "\n\n".join( - [ - f"check = {render_python(example.input_value)}\n" - f"print({function_name}(check)) # {render_python(example.expected_value)}" - for example in examples - ] - ) + blocks: list[str] = [] + + for example in examples: + lines: list[str] = [] + names = build_names("check", len(example.input_values)) + + for name, value in zip(names, example.input_values): + lines.append(f"{name} = {render_python(value)}") + + lines.append( + f"print({function_name}({', '.join(names)}))" + f" # {render_python(example.expected_value)}" + ) + blocks.append("\n".join(lines)) + + return "\n\n".join(blocks) def build_julia_checks(function_name: str, examples: list[Example]) -> str: - return "\n\n".join( - [ - f"check = {render_julia(example.input_value)}\n" - f"println({function_name}(check)) # {render_julia(example.expected_value)}" - for example in examples - ] - ) + blocks: list[str] = [] + for example in examples: + lines: list[str] = [] + names = build_names("check", len(example.input_values)) + + for name, value in zip(names, example.input_values): + lines.append(f"{name} = {render_julia(value)}") + + lines.append( + f"println({function_name}({', '.join(names)}))" + f" # {render_julia(example.expected_value)}" + ) + blocks.append("\n".join(lines)) + + return "\n\n".join(blocks) + +def render_rust_call_arg(name: str, value: object) -> str: + if isinstance(value, list): + return f"&{name}" + return name def build_rust_checks(function_name: str, examples: list[Example]) -> str: - return "\n\n".join( - [ - f" let check = {render_rust(example.input_value)};\n" - f' println!("{{:?}}", {function_name}(&check)); // {render_python(example.expected_value)}' - for example in examples - ] - ) + blocks: list[str] = [] + + for example in examples: + lines: list[str] = [] + names = build_names("check", len(example.input_values)) + + for name, value in zip(names, example.input_values): + lines.append(f" let {name} = {render_rust(value)};") + + call_args = ", ".join( + render_rust_call_arg(name, value) + for name, value in zip(names, example.input_values) + ) + lines.append( + f' println!("{{:?}}", {function_name}({call_args}));' + f" // {render_python(example.expected_value)}" + ) + blocks.append("\n".join(lines)) + + return "\n\n".join(blocks) def build_go_checks(function_name: str, examples: list[Example]) -> str: - checks: list[str] = [] + blocks: list[str] = [] - for index, example in enumerate(examples): - assign_op = ":=" if index == 0 else "=" - checks.append( - f" check {assign_op} {render_go(example.input_value)}\n" - f" fmt.Println({function_name}(check)) // {render_python(example.expected_value)}" - ) + for example in examples: + lines: list[str] = [" {"] + input_names = build_names("check", len(example.input_values)) - return "\n\n".join(checks) + for name, value in zip(input_names, example.input_values): + lines.append(f" {name} := {render_go(value)}") + if isinstance(example.expected_value, tuple): + result_names = build_names("result", len(example.expected_value)) + lines.append( + f" {', '.join(result_names)} := " + f"{function_name}({', '.join(input_names)})" + ) + lines.append( + f" fmt.Println({', '.join(result_names)})" + f" // {render_python(example.expected_value)}" + ) + else: + lines.append( + f" fmt.Println({function_name}({', '.join(input_names)}))" + f" // {render_python(example.expected_value)}" + ) + + lines.append(" }") + blocks.append("\n".join(lines)) + + return "\n\n".join(blocks) + +def default_python_value(value: object) -> str: + if isinstance(value, bool): + return "False" + if isinstance(value, int): + return "0" + if isinstance(value, str): + return '""' + if isinstance(value, list): + return "[]" + if isinstance(value, tuple): + return "(" + ", ".join(default_python_value(item) for item in value) + ")" + return "None" + + +def default_julia_value(value: object) -> str: + if isinstance(value, bool): + return "false" + if isinstance(value, int): + return "0" + if isinstance(value, str): + return '""' + if isinstance(value, list): + return "[]" + if isinstance(value, tuple): + return "(" + ", ".join(default_julia_value(item) for item in value) + ")" + return "nothing" + + +def default_rust_value(value: object) -> str: + if isinstance(value, bool): + return "false" + if isinstance(value, int): + return "0" + if isinstance(value, str): + return '""' + if isinstance(value, list): + return "vec![]" + if isinstance(value, tuple): + return "(" + ", ".join(default_rust_value(item) for item in value) + ")" + raise TypeError(f"Unsupported Rust type: {value!r}") + + +def default_go_value(value: object) -> str: + if isinstance(value, bool): + return "false" + if isinstance(value, int): + return "0" + if isinstance(value, str): + return '""' + if isinstance(value, list): + return f"{infer_go_type(value)}{{}}" + if isinstance(value, tuple): + return ", ".join(default_go_value(item) for item in value) + raise TypeError(f"Unsupported Go type: {value!r}") + + +def infer_go_signature_return(value: object) -> str: + if isinstance(value, tuple): + return "(" + ", ".join(infer_go_type(item) for item in value) + ")" + return infer_go_type(value) def comment_block(prefix: str, text: str) -> str: return "\n".join( @@ -181,19 +363,34 @@ def comment_block(prefix: str, text: str) -> str: for line in text.splitlines() ) +def create_if_missing( + output_path: Path, + creator: Callable[[Path, str, list[Example]], None], + function_name: str, + examples: list[Example], +) -> None: + if output_path.exists(): + return + + output_path.parent.mkdir(parents=True, exist_ok=True) + creator(output_path, function_name, examples) def create_python( problem_py: Path, function_name: str, examples: list[Example], ) -> None: - input_type = infer_python_type(examples[0].input_value) + input_names = build_names("value", len(examples[0].input_values)) + params = ", ".join( + f"{name}: {infer_python_type(value)}" + for name, value in zip(input_names, examples[0].input_values) + ) return_type = infer_python_type(examples[0].expected_value) template = "\n".join( [ - f"def {function_name}(values: {input_type}) -> {return_type}:", - " return []", + f"def {function_name}({params}) -> {return_type}:", + f" return {default_python_value(examples[0].expected_value)}", "", build_python_checks(function_name, examples), "", @@ -207,10 +404,12 @@ def create_julia( function_name: str, examples: list[Example], ) -> None: + params = ", ".join(build_names("value", len(examples[0].input_values))) + template = "\n".join( [ - f"function {function_name}(values)", - " return []", + f"function {function_name}({params})", + f" return {default_julia_value(examples[0].expected_value)}", "end", "", build_julia_checks(function_name, examples), @@ -225,14 +424,17 @@ def create_rust( function_name: str, examples: list[Example], ) -> None: - input_type = infer_rust_param_type(examples[0].input_value) + input_names = build_names("value", len(examples[0].input_values)) + params = ", ".join( + f"{name}: {infer_rust_param_type(value)}" + for name, value in zip(input_names, examples[0].input_values) + ) return_type = infer_rust_type(examples[0].expected_value) template = "\n".join( [ - f"fn {function_name}(values: {input_type}) -> {return_type} {{", - " let _ = values;", - " vec![]", + f"fn {function_name}({params}) -> {return_type} {{", + f" {default_rust_value(examples[0].expected_value)}", "}", "", "fn main() {", @@ -249,8 +451,12 @@ def create_go( function_name: str, examples: list[Example], ) -> None: - input_type = infer_go_type(examples[0].input_value) - return_type = infer_go_type(examples[0].expected_value) + input_names = build_names("value", len(examples[0].input_values)) + params = ", ".join( + f"{name} {infer_go_type(value)}" + for name, value in zip(input_names, examples[0].input_values) + ) + return_type = infer_go_signature_return(examples[0].expected_value) template = "\n".join( [ @@ -258,9 +464,8 @@ def create_go( "", 'import "fmt"', "", - f"func {function_name}(values {input_type}) {return_type} " + "{", - " _ = values", - f" return {return_type}" + "{}", + f"func {function_name}({params}) {return_type} " + "{", + f" return {default_go_value(examples[0].expected_value)}", "}", "", "func main() {", @@ -288,18 +493,14 @@ if __name__ == "__main__": instructions = load_instructions(base_dir / "instructions.txt") problem_stem = f"{args.problem_number:03d}" - problem_py = base_dir / f"{problem_stem}.py" - problem_jl = base_dir / f"{problem_stem}.jl" - problem_rs = base_dir / f"{problem_stem}.rs" - problem_go = base_dir / f"{problem_stem}.go" + problem_py = base_dir / "Python" / f"{problem_stem}.py" + problem_jl = base_dir / "Julia" / f"{problem_stem}.jl" + problem_rs = base_dir / "Rust" / f"{problem_stem}.rs" + problem_go = base_dir / "Go" / f"{problem_stem}.go" function_name, examples = parse_instructions(instructions) - create_python(problem_py, function_name, examples) - create_julia(problem_jl, function_name, examples) - create_rust(problem_rs, function_name, examples) - create_go(problem_go, function_name, examples) - shutil.move(problem_py, base_dir / "Python") - shutil.move(problem_jl, base_dir / "Julia") - shutil.move(problem_rs, base_dir / "Rust") - shutil.move(problem_go, base_dir / "Go") \ No newline at end of file + create_if_missing(problem_py, create_python, function_name, examples) + create_if_missing(problem_jl, create_julia, function_name, examples) + create_if_missing(problem_rs, create_rust, function_name, examples) + create_if_missing(problem_go, create_go, function_name, examples) \ No newline at end of file