Skip to content

Commit

Permalink
Add generator (from x86-64-assembly) (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
keiravillekode authored Oct 17, 2024
1 parent 12e6263 commit f430e7f
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build/
**/tests
**/*.o
*.pyc
__pycache__/
36 changes: 36 additions & 0 deletions bin/create-exercise
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
11 changes: 6 additions & 5 deletions exercises/practice/hello-world/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions exercises/practice/hello-world/.meta/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
13 changes: 13 additions & 0 deletions exercises/practice/hello-world/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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!"
2 changes: 0 additions & 2 deletions exercises/practice/hello-world/hello_world_test.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Version: 1.1.0

#include "vendor/unity.h"

extern const char *hello(void);
Expand Down
8 changes: 8 additions & 0 deletions generators/exercises/hello_world.py
Original file line number Diff line number Diff line change
@@ -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'
130 changes: 130 additions & 0 deletions generators/generate
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit f430e7f

Please sign in to comment.