From 03d1661cb632db84e205fdc9831908a8566a9572 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Sat, 24 Oct 2026 16:15:34 -0700 Subject: [PATCH] Implement representer script Dockerfile and other bin/ files to follow --- bin/run.py | 147 ++++++++++++++++++ bin/run.sh | 30 +--- .../multiple-solution-files/.meta/config.json | 13 ++ .../expected_mapping.json | 5 +- .../expected_representation.json | 4 +- .../expected_representation.txt | 16 ++ tests/multiple-solution-files/one.gd | 2 + tests/multiple-solution-files/two.gd | 2 + .../expected_mapping.json | 6 +- .../expected_representation.json | 4 +- .../expected_representation.txt | 22 ++- .../normalize_whitespace.gd | 5 + tests/remove-comments/expected_mapping.json | 6 +- .../expected_representation.json | 4 +- .../expected_representation.txt | 21 ++- tests/remove-comments/remove_comments.gd | 10 ++ .../rename-identifiers/expected_mapping.json | 7 + .../expected_representation.json | 3 + .../expected_representation.txt | 34 ++++ .../rename-identifiers/rename_identifiers.gd | 6 + 20 files changed, 311 insertions(+), 36 deletions(-) create mode 100755 bin/run.py create mode 100644 tests/multiple-solution-files/.meta/config.json create mode 100644 tests/multiple-solution-files/one.gd create mode 100644 tests/multiple-solution-files/two.gd create mode 100644 tests/normalize-whitespace/normalize_whitespace.gd create mode 100644 tests/remove-comments/remove_comments.gd create mode 100644 tests/rename-identifiers/expected_mapping.json create mode 100644 tests/rename-identifiers/expected_representation.json create mode 100644 tests/rename-identifiers/expected_representation.txt create mode 100644 tests/rename-identifiers/rename_identifiers.gd diff --git a/bin/run.py b/bin/run.py new file mode 100755 index 0000000..ce81697 --- /dev/null +++ b/bin/run.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +import argparse +import json +import pathlib +import sys + +import gdtoolkit.parser +from lark import Token, Tree + +def load_config(input_path: pathlib.Path) -> dict | None: + config_path = input_path / ".meta/config.json" + if not config_path.exists(): + return None + return json.loads((input_path / ".meta/config.json").read_text()) + + +def solution_files(slug: str, input_path: pathlib.Path) -> list[pathlib.Path]: + config = load_config(input_path) + if config: + files = config.get("files", {}).get("solution", []) + if files: + return [input_path / file for file in files] + return [(input_path / slug.replace("-", "_")).with_suffix(".gd")] + + +def load_ast(slug: str, input_path: pathlib.Path) -> list[Tree]: + return [ + gdtoolkit.parser.parser.parse(file.read_text()) + for file in solution_files(slug, input_path) + ] + + +def drop_docstr(trees: list[Tree]) -> list[Tree]: + def skip(t): + if isinstance(t, Tree) and t.data == "docstr_stmt": + return True + if ( + isinstance(t, Tree) + and t.data == "expr_stmt" + and len(t.children) == 1 + and isinstance(t.children[0], Tree) + and t.children[0].data == "expr" + and len(t.children[0].children) == 1 + and isinstance(t.children[0].children[0], Tree) + and t.children[0].children[0].data == "string" + ): + return True + return False + + def translate(t): + if isinstance(t, Token): + return t + elif isinstance(t, Tree): + return Tree(t.data, [translate(i) for i in t.children if not skip(i)]) + else: + raise ValueError(t) + + return [translate(tree) for tree in trees] + + +def replace_func_names(trees: list[Tree]) -> tuple[list[Tree], dict[str, str]]: + varmap = {} + for tree in trees: + func_names = [ + next(i.find_token("NAME")) + for i in tree.find_data(Token("RULE", "func_header")) + ] + for var in func_names: + if var.value not in varmap: + varmap[var.value] = f"FUNC_{len(varmap) + 1:03d}" + + def translate(t): + if isinstance(t, Token): + return Token(t.type, varmap.get(t.value, t.value)) + elif isinstance(t, Tree): + return Tree(t.data, [translate(i) for i in t.children]) + else: + raise ValueError(t) + + revmap = {v: k for k, v in varmap.items()} + + trees = [translate(tree) for tree in trees] + return trees, revmap + + +def replace_var_names(trees: list[Tree]) -> tuple[list[Tree], dict[str, str]]: + varmap = {} + for tree in trees: + func_vars = [ + next(i.find_token("NAME")) + for token_type in ["func_var_stmt", "func_arg_typed", "func_arg_regular"] + for i in tree.find_data(Token("RULE", token_type)) + ] + func_vars.sort(key=lambda x: x.line) + for var in func_vars: + if var.value not in varmap: + varmap[var.value] = f"FUNCVAR_{len(varmap) + 1:03d}" + + def translate(t): + if isinstance(t, Token): + return Token(t.type, varmap.get(t.value, t.value)) + elif isinstance(t, Tree): + return Tree(t.data, [translate(i) for i in t.children]) + else: + raise ValueError(t) + + revmap = {v: k for k, v in varmap.items()} + + trees = [translate(tree) for tree in trees] + return trees, revmap + + +def write_output(output_path: pathlib.Path, trees: list[Tree], varmap: dict[str, str]) -> None: + if not output_path.exists(): + output_path.mkdir(parents=Tree, exist_ok=True) + + (output_path / "representation.json").write_text(json.dumps({"version": 1}, indent=2) + "\n") + (output_path / "mapping.json").write_text(json.dumps(varmap, indent=2, sort_keys=True) + "\n") + (output_path / "representation.txt").write_text("\n\n".join(tree.pretty() for tree in trees)) + + + +def arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("slug") + parser.add_argument("input_dir") + parser.add_argument("output_dir") + return parser + + +def main() -> None: + args = arg_parser().parse_args() + input_path = pathlib.Path(args.input_dir) + output_path = pathlib.Path(args.output_dir) + + print(f"== Creating representation for {args.slug!r} ==") + trees = load_ast(args.slug, input_path) + trees = drop_docstr(trees) + trees, varmap = replace_var_names(trees) + trees, funcmap = replace_func_names(trees) + write_output(output_path, trees, varmap | funcmap) + print(f"== Done with {args.slug!r} ==") + + +if __name__ == "__main__": + main() diff --git a/bin/run.sh b/bin/run.sh index 4b88d67..a493216 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # Synopsis: # Run the representer on a solution. @@ -15,30 +15,4 @@ # Example: # ./bin/run.sh two-fer /absolute/path/to/two-fer/solution/folder/ /absolute/path/to/output/directory/ -# If any required arguments is missing, print the usage and exit -if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then - echo "usage: ./bin/run.sh exercise-slug /absolute/path/to/two-fer/solution/folder/ /absolute/path/to/output/directory/" - exit 1 -fi - -slug="$1" -input_dir="${2%/}" -output_dir="${3%/}" -representation_txt_file="${output_dir}/representation.txt" -representation_json_file="${output_dir}/representation.json" -mapping_file="${output_dir}/mapping.json" - -# Create the output directory if it doesn't exist -mkdir -p "${output_dir}" - -echo "${slug}: creating representation..." - -# Create the representation for the solution -# TODO: replace the below commands with your own commands -# to create the representation.txt, representation.json and -# mapping.json files -echo '' > "${representation_txt_file}" -echo '{"version": 1}' > "${representation_json_file}" -echo '{}' > "${mapping_file}" - -echo "${slug}: done" +python3 "${0%.*}.py" "$@" diff --git a/tests/multiple-solution-files/.meta/config.json b/tests/multiple-solution-files/.meta/config.json new file mode 100644 index 0000000..1f424d6 --- /dev/null +++ b/tests/multiple-solution-files/.meta/config.json @@ -0,0 +1,13 @@ +{ + "authors": [ + "person" + ], + "files": { + "solution": [ + "one.gd", + "two.gd" + ], + "test": [], + "example": [] + } +} diff --git a/tests/multiple-solution-files/expected_mapping.json b/tests/multiple-solution-files/expected_mapping.json index 0967ef4..dd43d40 100644 --- a/tests/multiple-solution-files/expected_mapping.json +++ b/tests/multiple-solution-files/expected_mapping.json @@ -1 +1,4 @@ -{} +{ + "FUNC_001": "foo", + "FUNC_002": "bar" +} diff --git a/tests/multiple-solution-files/expected_representation.json b/tests/multiple-solution-files/expected_representation.json index cb608f6..61a2092 100644 --- a/tests/multiple-solution-files/expected_representation.json +++ b/tests/multiple-solution-files/expected_representation.json @@ -1 +1,3 @@ -{"version": 1} +{ + "version": 1 +} diff --git a/tests/multiple-solution-files/expected_representation.txt b/tests/multiple-solution-files/expected_representation.txt index 8b13789..e1ddaec 100644 --- a/tests/multiple-solution-files/expected_representation.txt +++ b/tests/multiple-solution-files/expected_representation.txt @@ -1 +1,17 @@ +start + func_def + func_header + FUNC_001 + func_args + return_stmt + expr 1 + +start + func_def + func_header + FUNC_002 + func_args + return_stmt + expr + string "Hello" diff --git a/tests/multiple-solution-files/one.gd b/tests/multiple-solution-files/one.gd new file mode 100644 index 0000000..0d59bd4 --- /dev/null +++ b/tests/multiple-solution-files/one.gd @@ -0,0 +1,2 @@ +func foo(): + return 1 diff --git a/tests/multiple-solution-files/two.gd b/tests/multiple-solution-files/two.gd new file mode 100644 index 0000000..dd34d33 --- /dev/null +++ b/tests/multiple-solution-files/two.gd @@ -0,0 +1,2 @@ +func bar(): + return "Hello" diff --git a/tests/normalize-whitespace/expected_mapping.json b/tests/normalize-whitespace/expected_mapping.json index 0967ef4..c4292ce 100644 --- a/tests/normalize-whitespace/expected_mapping.json +++ b/tests/normalize-whitespace/expected_mapping.json @@ -1 +1,5 @@ -{} +{ + "FUNCVAR_001": "a", + "FUNCVAR_002": "b", + "FUNC_001": "foo" +} diff --git a/tests/normalize-whitespace/expected_representation.json b/tests/normalize-whitespace/expected_representation.json index cb608f6..61a2092 100644 --- a/tests/normalize-whitespace/expected_representation.json +++ b/tests/normalize-whitespace/expected_representation.json @@ -1 +1,3 @@ -{"version": 1} +{ + "version": 1 +} diff --git a/tests/normalize-whitespace/expected_representation.txt b/tests/normalize-whitespace/expected_representation.txt index 8b13789..b085c1b 100644 --- a/tests/normalize-whitespace/expected_representation.txt +++ b/tests/normalize-whitespace/expected_representation.txt @@ -1 +1,21 @@ - +start + func_def + func_header + FUNC_001 + func_args + func_var_stmt + func_var_assigned + FUNCVAR_001 + expr + arith_expr + 1 + + + 2 + func_var_stmt + func_var_assigned + FUNCVAR_002 + expr + arith_expr + 3 + + + 4 diff --git a/tests/normalize-whitespace/normalize_whitespace.gd b/tests/normalize-whitespace/normalize_whitespace.gd new file mode 100644 index 0000000..71a4147 --- /dev/null +++ b/tests/normalize-whitespace/normalize_whitespace.gd @@ -0,0 +1,5 @@ +func foo(): + var a = 1 + 2 + + + var b = 3 + 4 diff --git a/tests/remove-comments/expected_mapping.json b/tests/remove-comments/expected_mapping.json index 0967ef4..c4292ce 100644 --- a/tests/remove-comments/expected_mapping.json +++ b/tests/remove-comments/expected_mapping.json @@ -1 +1,5 @@ -{} +{ + "FUNCVAR_001": "a", + "FUNCVAR_002": "b", + "FUNC_001": "foo" +} diff --git a/tests/remove-comments/expected_representation.json b/tests/remove-comments/expected_representation.json index cb608f6..61a2092 100644 --- a/tests/remove-comments/expected_representation.json +++ b/tests/remove-comments/expected_representation.json @@ -1 +1,3 @@ -{"version": 1} +{ + "version": 1 +} diff --git a/tests/remove-comments/expected_representation.txt b/tests/remove-comments/expected_representation.txt index 8b13789..c114765 100644 --- a/tests/remove-comments/expected_representation.txt +++ b/tests/remove-comments/expected_representation.txt @@ -1 +1,20 @@ - +start + func_def + func_header + FUNC_001 + func_args + func_var_stmt + func_var_assigned + FUNCVAR_001 + expr + arith_expr + 1 + + + 2 + func_var_stmt + func_var_assigned + FUNCVAR_002 + expr + string "Hello World" + return_stmt + expr FUNCVAR_001 diff --git a/tests/remove-comments/remove_comments.gd b/tests/remove-comments/remove_comments.gd new file mode 100644 index 0000000..5a5708e --- /dev/null +++ b/tests/remove-comments/remove_comments.gd @@ -0,0 +1,10 @@ +"""Module docstring.""" +# Module comment + +func foo(): + """Function docstring.""" + # Function comment + var a = 1 + 2 + "Some other string" + var b = "Hello World" + return a diff --git a/tests/rename-identifiers/expected_mapping.json b/tests/rename-identifiers/expected_mapping.json new file mode 100644 index 0000000..9c35922 --- /dev/null +++ b/tests/rename-identifiers/expected_mapping.json @@ -0,0 +1,7 @@ +{ + "FUNCVAR_001": "name", + "FUNCVAR_002": "suffix", + "FUNCVAR_003": "greeting", + "FUNC_001": "greet", + "FUNC_002": "greet_bob" +} diff --git a/tests/rename-identifiers/expected_representation.json b/tests/rename-identifiers/expected_representation.json new file mode 100644 index 0000000..61a2092 --- /dev/null +++ b/tests/rename-identifiers/expected_representation.json @@ -0,0 +1,3 @@ +{ + "version": 1 +} diff --git a/tests/rename-identifiers/expected_representation.txt b/tests/rename-identifiers/expected_representation.txt new file mode 100644 index 0000000..f10f63b --- /dev/null +++ b/tests/rename-identifiers/expected_representation.txt @@ -0,0 +1,34 @@ +start + func_def + func_header + FUNC_001 + func_args + func_arg_typed + FUNCVAR_001 + str + func_arg_regular FUNCVAR_002 + func_var_stmt + func_var_assigned + FUNCVAR_003 + expr + string "Hello " + expr_stmt + expr + standalone_call + print + arith_expr + FUNCVAR_003 + + + FUNCVAR_001 + + + FUNCVAR_002 + func_def + func_header + FUNC_002 + func_args + expr_stmt + expr + standalone_call + FUNC_001 + string "Bob" + string "!" diff --git a/tests/rename-identifiers/rename_identifiers.gd b/tests/rename-identifiers/rename_identifiers.gd new file mode 100644 index 0000000..62ab62a --- /dev/null +++ b/tests/rename-identifiers/rename_identifiers.gd @@ -0,0 +1,6 @@ +func greet(name: str, suffix): + var greeting = "Hello " + print(greeting + name + suffix) + +func greet_bob(): + greet("Bob", "!")