From 35df74a34f74ef952f4cff4c3f995142e29355ac Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sun, 13 Oct 2024 20:27:00 +1100 Subject: [PATCH] Add generator (from x86-64-assembly) --- .gitignore | 1 + bin/create-exercise | 36 +++++ config.json | 8 +- .../hello-world/.docs/instructions.md | 11 +- .../practice/hello-world/.meta/config.json | 4 +- .../practice/hello-world/.meta/tests.toml | 13 ++ .../practice/hello-world/hello_world_test.c | 2 - generators/exercises/hello_world.py | 8 ++ generators/generate | 130 ++++++++++++++++++ 9 files changed, 200 insertions(+), 13 deletions(-) create mode 100755 bin/create-exercise create mode 100644 exercises/practice/hello-world/.meta/tests.toml create mode 100644 generators/exercises/hello_world.py create mode 100755 generators/generate diff --git a/.gitignore b/.gitignore index 2b463aa..211f5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ **/tests **/*.o *.pyc +__pycache__/ diff --git a/bin/create-exercise b/bin/create-exercise new file mode 100755 index 0000000..597cc29 --- /dev/null +++ b/bin/create-exercise @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -e + +die() { echo "$*" >&2; exit 1; } + +if [[ $PWD != $(realpath "$(dirname "$0")/..") ]]; then + die "You must be in the track root directory." +fi +if [[ -z $1 ]]; then + die "usage: $0 exercise_slug" +fi + +slug=$1 + +existing=$( jq --arg slug "${slug}" '.exercises.practice[] | select(.slug == $slug)' config.json ) +if [[ -n ${existing} ]]; then + die "${slug} already exists in config.json" +fi + +pascal=${slug^} +while [[ ${pascal} =~ (.*)-(.*) ]]; do + pascal=${BASH_REMATCH[1]}${BASH_REMATCH[2]^} +done + +if [[ -z $author ]]; then + echo + read -rp "What's your github username? " author +fi + +read -p "What's the difficulty for ${slug}? " difficulty + +bin/fetch-configlet +bin/configlet create --practice-exercise "${slug}" --author "${author}" --difficulty "${difficulty}" + +cp exercises/practice/hello-world/Makefile exercises/practice/${slug}/Makefile +cp -r exercises/practice/hello-world/vendor exercises/practice/${slug}/vendor diff --git a/config.json b/config.json index 759a9c9..5c3c9d0 100644 --- a/config.json +++ b/config.json @@ -49,16 +49,16 @@ "concepts": [], "key_features": [], "tags": [ + "execution_mode/compiled", "paradigm/imperative", "paradigm/procedural", - "typing/static", - "typing/weak", - "execution_mode/compiled", "platform/android", "platform/ios", - "platform/mac", "platform/linux", + "platform/mac", "runtime/standalone_executable", + "typing/static", + "typing/weak", "used_for/embedded_systems", "used_for/mobile" ] diff --git a/exercises/practice/hello-world/.docs/instructions.md b/exercises/practice/hello-world/.docs/instructions.md index 6e08ebb..c9570e4 100644 --- a/exercises/practice/hello-world/.docs/instructions.md +++ b/exercises/practice/hello-world/.docs/instructions.md @@ -1,15 +1,16 @@ # Instructions -The classical introductory exercise. Just say "Hello, World!". +The classical introductory exercise. +Just say "Hello, World!". -["Hello, World!"](http://en.wikipedia.org/wiki/%22Hello,_world!%22_program) is -the traditional first program for beginning programming in a new language -or environment. +["Hello, World!"][hello-world] is the traditional first program for beginning programming in a new language or environment. The objectives are simple: -- Write a function that returns the string "Hello, World!". +- Modify the provided code so that it produces the string "Hello, World!". - Run the test suite and make sure that it succeeds. - Submit your solution and check it at the website. If everything goes well, you will be ready to fetch your first real exercise. + +[hello-world]: https://en.wikipedia.org/wiki/%22Hello,_world!%22_program diff --git a/exercises/practice/hello-world/.meta/config.json b/exercises/practice/hello-world/.meta/config.json index 985442f..395b7f4 100644 --- a/exercises/practice/hello-world/.meta/config.json +++ b/exercises/practice/hello-world/.meta/config.json @@ -13,7 +13,7 @@ ".meta/example.s" ] }, - "blurb": "The classical introductory exercise. Just say \"Hello, World!\"", + "blurb": "Exercism's classic introductory exercise. Just say \"Hello, World!\".", "source": "This is an exercise to introduce users to using Exercism", - "source_url": "http://en.wikipedia.org/wiki/%22Hello,_world!%22_program" + "source_url": "https://en.wikipedia.org/wiki/%22Hello,_world!%22_program" } diff --git a/exercises/practice/hello-world/.meta/tests.toml b/exercises/practice/hello-world/.meta/tests.toml new file mode 100644 index 0000000..73466d6 --- /dev/null +++ b/exercises/practice/hello-world/.meta/tests.toml @@ -0,0 +1,13 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[af9ffe10-dc13-42d8-a742-e7bdafac449d] +description = "Say Hi!" diff --git a/exercises/practice/hello-world/hello_world_test.c b/exercises/practice/hello-world/hello_world_test.c index f30cfdc..ed0ab18 100644 --- a/exercises/practice/hello-world/hello_world_test.c +++ b/exercises/practice/hello-world/hello_world_test.c @@ -1,5 +1,3 @@ -// Version: 1.1.0 - #include "vendor/unity.h" extern const char *hello(void); diff --git a/generators/exercises/hello_world.py b/generators/exercises/hello_world.py new file mode 100644 index 0000000..6b7efb2 --- /dev/null +++ b/generators/exercises/hello_world.py @@ -0,0 +1,8 @@ +FUNC_PROTO = """\ +#include "vendor/unity.h" + +extern const char *hello(void); +""" + +def gen_func_body(prop, inp, expected): + return f'TEST_ASSERT_EQUAL_STRING("{expected}", {prop}());\n' diff --git a/generators/generate b/generators/generate new file mode 100755 index 0000000..e022e48 --- /dev/null +++ b/generators/generate @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +import argparse +import importlib +import json +import re +import subprocess +import sys +import textwrap +import tomllib + + +def camel_to_snake(s): + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", s) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + +def desc_to_funcname(desc): + s = "test_" + desc.replace(" ", "_").lower() + return re.sub("[^a-z0-9_]", "", s) + + +def read_canonical_data(exercise): + prefix = "Using cached 'problem-specifications' dir: " + args = ['bin/configlet', 'info', '-o', '-v', 'd'] + info = subprocess.run(args, capture_output=True, check=True, text=True).stdout.split("\n") + cache_dir = [line[len(prefix):] for line in info if line.startswith(prefix)] + if len(cache_dir) != 1: + raise Exception("Could not determine 'problem-specifications' dir") + path = f"{cache_dir[0]}/exercises/{exercise}/canonical-data.json" + with open(path, "r") as f: + return json.loads(f.read()) + + +def flatten_cases(data): + cases_by_id = {} + + def traverse(node): + nonlocal cases_by_id + if "cases" in node: + for child in node["cases"]: + traverse(child) + else: + add_case(cases_by_id, node) + + traverse(data) + return [cases_by_id[uuid] for uuid in cases_by_id] + + +def add_case(cases_by_id, case): + if "reimplements" in case: + cases_by_id[case["reimplements"]] = case + else: + cases_by_id[case["uuid"]] = case + + +def gen_funcname(mod, case): + if hasattr(mod, "describe"): + description = mod.describe(case) + else: + description = case["description"] + return desc_to_funcname(description) + + +def gen_main(mod, cases): + str_list = [] + str_list.append("int main(void) {\n UNITY_BEGIN();\n") + for case in cases: + funcname = gen_funcname(mod, case) + str_list.append(f" RUN_TEST({funcname});\n") + str_list.append(" return UNITY_END();\n}\n") + return "".join(str_list) + + +def gen_test_case(mod, case, test_ignore): + str_list = [] + funcname = gen_funcname(mod, case) + str_list.append(f"void {funcname}(void) {{\n") + if test_ignore: + str_list.append(" TEST_IGNORE();\n") + prop = camel_to_snake(case["property"]) + body = mod.gen_func_body(prop, case["input"], case["expected"]) + body = textwrap.indent(body, " ") + str_list.append(f"{body}}}\n\n") + return "".join(str_list) + + +def gen_test_cases(mod, cases): + str_list = [] + test_ignore = False + for case in cases: + str_list.append(gen_test_case(mod, case, test_ignore)) + test_ignore = True + return "".join(str_list) + + +def gen_test_file(mod, cases): + str_list = [] + str_list.append(mod.FUNC_PROTO) + str_list.append("\nvoid setUp(void) {\n}\n\nvoid tearDown(void) {\n}\n\n") + str_list.append(gen_test_cases(mod, cases)) + str_list.append(gen_main(mod, cases)) + return "".join(str_list) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("exercise") + parser.add_argument("-i", "--ignore-toml", action="store_true") + args = parser.parse_args() + data = read_canonical_data(args.exercise) + cases = flatten_cases(data) + if not args.ignore_toml: + test_toml = f"{sys.path[0]}/../exercises/practice/{args.exercise}/.meta/tests.toml" + with open(test_toml, "rb") as f: + test_toml = tomllib.load(f) + cases = list(filter(lambda case : test_toml.get(case['uuid'],{}).get('include', True), cases)) + exercise = args.exercise.replace("-", "_") + mod = importlib.import_module("exercises." + exercise) + if hasattr(mod, "extra_cases"): + cases = cases[:] + cases.extend(mod.extra_cases()) + s = gen_test_file(mod, cases) + path = f"{sys.path[0]}/../exercises/practice/{args.exercise}/{exercise}_test.c" + with open(path, "w") as f: + f.write(s) + + +if __name__ == "__main__": + main()