diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..8bf7ae71 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,35 @@ +name: Pre-commit Checks + +permissions: + contents: read + pull-requests: write + +on: + pull_request: + branches: [ main ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - uses: pre-commit/action@v3.0.0 + id: precommit + continue-on-error: true + - name: Comment on PR if pre-commit failed + if: steps.precommit.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ Pre-commit checks failed. Please run `pre-commit run --all-files` locally and fix the issues.' + }) + - name: Exit with pre-commit status + if: steps.precommit.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..157c993d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + strategy: + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[test]" + + - name: Run tests + run: | + pytest diff --git a/.gitignore b/.gitignore index f6bb7855..c3ff9440 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ ENV/ htmlcov/ .pytest_cache/ -.DS_Store \ No newline at end of file +# OSX +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72c22f89..49987591 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,3 +18,4 @@ repos: hooks: - id: isort name: isort (python) +exclude: GALLERY.md diff --git a/GALLERY.md b/GALLERY.md index f76b0304..56e5836f 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -2,41 +2,210 @@ This gallery shows examples from all available datasets using their default configurations. ## Available Datasets -- [base_conversion](#base-conversion) -- [basic_arithmetic](#basic-arithmetic) -- [caesar_cipher](#caesar-cipher) -- [chain_sum](#chain-sum) -- [color_cube_rotation](#color-cube-rotation) +- [advanced_geometry](#advanced_geometry) +- [aiw](#aiw) +- [arc_1d](#arc_1d) +- [base_conversion](#base_conversion) +- [basic_arithmetic](#basic_arithmetic) +- [bf](#bf) +- [caesar_cipher](#caesar_cipher) +- [calendar_arithmetic](#calendar_arithmetic) +- [chain_sum](#chain_sum) +- [color_cube_rotation](#color_cube_rotation) - [countdown](#countdown) -- [family_relationships](#family-relationships) -- [figlet_font](#figlet-font) -- [fraction_simplification](#fraction-simplification) +- [family_relationships](#family_relationships) +- [figlet_font](#figlet_font) +- [fraction_simplification](#fraction_simplification) +- [game_of_life](#game_of_life) - [gcd](#gcd) +- [intermediate_integration](#intermediate_integration) - [lcm](#lcm) -- [leg_counting](#leg-counting) -- [letter_counting](#letter-counting) -- [letter_jumble](#letter-jumble) +- [leg_counting](#leg_counting) +- [letter_counting](#letter_counting) +- [letter_jumble](#letter_jumble) - [maze](#maze) -- [mini_sudoku](#mini-sudoku) -- [number_filtering](#number-filtering) -- [number_sequence](#number-sequence) -- [number_sorting](#number-sorting) -- [polynomial_equations](#polynomial-equations) -- [prime_factorization](#prime-factorization) -- [propositional_logic](#propositional-logic) -- [quantum_lock](#quantum-lock) -- [rubiks_cube](#rubiks-cube) -- [sentence_reordering](#sentence-reordering) -- [simple_equations](#simple-equations) -- [spell_backward](#spell-backward) +- [mini_sudoku](#mini_sudoku) +- [n_queens](#n_queens) +- [number_filtering](#number_filtering) +- [number_sequence](#number_sequence) +- [number_sorting](#number_sorting) +- [polynomial_equations](#polynomial_equations) +- [prime_factorization](#prime_factorization) +- [propositional_logic](#propositional_logic) +- [quantum_lock](#quantum_lock) +- [rubiks_cube](#rubiks_cube) +- [sentence_reordering](#sentence_reordering) +- [simple_equations](#simple_equations) +- [simple_geometry](#simple_geometry) +- [simple_integration](#simple_integration) +- [spell_backward](#spell_backward) - [sudoku](#sudoku) - [syllogism](#syllogism) -- [word_ladder](#word-ladder) -- [word_sequence_reversal](#word-sequence-reversal) -- [word_sorting](#word-sorting) +- [time_intervals](#time_intervals) +- [tower_of_hanoi](#tower_of_hanoi) +- [word_ladder](#word_ladder) +- [word_sequence_reversal](#word_sequence_reversal) +- [word_sorting](#word_sorting) ## Dataset Examples -### base_conversion {base-conversion} +### advanced_geometry +A dataset for advanced geometry tasks using coordinate geometry. + +Default configuration: +```python +min_coord = -10 +max_coord = 10 +size = 50 +seed = 42 +task_types = ['orthocenter', 'incircle_radius', 'angle_measure'] +``` + +Example tasks: +```` +Example 1: +Question: In triangle ABC with coordinates A=(-7, -10), B=(-2, -3), and C=(-3, -6), find the measure (in degrees) of angle ABC. +Answer: 17.10° +Metadata: {'A': (-7, -10), 'B': (-2, -3), 'C': (-3, -6), 'angle_ABC_degrees': 17.10272896905237} + +Example 2: +Question: For triangle with vertices A=(-1, -6), B=(4, 1), and C=(-7, 4), determine the orthocenter (intersection of altitudes). +Answer: (0.304, -1.217) +Metadata: {'A': (-1, -6), 'B': (4, 1), 'C': (-7, 4), 'orthocenter_exact': ('7/23', '-28/23'), 'orthocenter_approx': (0.30434782608695654, -1.2173913043478262)} + +Example 3: +Question: Find the incircle radius of triangle ABC whose vertices are A=(6, 7), B=(-7, -5), and C=(2, -3). +Answer: 2.176 +Metadata: {'A': (6, 7), 'B': (-7, -5), 'C': (2, -3), 'incircle_radius_exact': 'sqrt(-sqrt(29) + sqrt(85)/2 + sqrt(313)/2)*sqrt(-sqrt(313)/2 + sqrt(85)/2 + sqrt(29))*sqrt(-sqrt(85)/2 + sqrt(29) + sqrt(313)/2)/sqrt(sqrt(85)/2 + sqrt(29) + sqrt(313)/2)', 'incircle_radius_approx': 2.176123777286009} + +```` + +### aiw +A procedural dataset inspired by the "Alice in Wonderland" paper. + + The dataset is inspired by the following paper: + @inproceedings{nezhurina2024alice, + title={Alice in Wonderland: Simple Tasks Reveal Severe Generalization and + Basic Reasoning Deficits in State-Of-the-Art Large Language Models}, + author={Marianna Nezhurina and Lucia Cipolina-Kun and Mehdi Cherti and + Jenia Jitsev}, + booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding + Deep Learning}, + year={2024}, + url={https://openreview.net/forum?id=Mkl7dzjYiW} + } + +Default configuration: +```python +male_names = ['James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles', 'Bob'] +female_names = ['Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Margaret', 'Alice'] +task_types = [, , ] +seed = 42 +size = 10 +max_entities = 6 +``` + +Example tasks: +```` +Example 1: +Question: Patricia has 6 male colleagues and she also has 3 female colleagues. These are all colleagues that Patricia has. All these mentioned persons around Patricia are colleagues of each other. James has 2 male colleagues and 2 female colleagues in total. All these mentioned persons around James are colleagues of each other. The people in the circle around James do not have other colleagues aside - with the only exception of Matilda. She is colleague of James and she is also colleague of Patricia, being part of Patricia's circle. How many female colleagues does Matilda have? +Answer: 4 +Metadata: {'task_type': 'colleagues'} + +Example 2: +Question: Elizabeth has 4 brothers and she also has 3 sisters. How many sisters does Elizabeth's brother have? +Answer: 4 +Metadata: {'task_type': 'siblings'} + +Example 3: +Question: Sarah has 6 male friends and she also has 1 female friends. They all are friends with each other and have no other friends aside. How many female friends does Thomas, a male friend of Sarah, have? +Answer: 2 +Metadata: {'task_type': 'friends'} + +```` + +### arc_1d +Generates ARC 1D tasks by randomly selecting from available task generators + +Default configuration: +```python +min_size = 10 +max_size = 30 +num_train = 3 +seed = 42 +size = 500 +``` + +Example tasks: +```` +Example 1: +Question: Find the common rule that maps an input grid to an output grid, given the examples below. + +Example 1: +Input: 7 1 0 0 5 5 0 5 5 0 0 0 0 +Output: 7 1 0 0 7 7 0 1 1 0 0 0 0 + +Example 2: +Input: 5 1 0 5 5 0 5 5 0 0 0 0 0 +Output: 5 1 0 5 5 0 1 1 0 0 0 0 0 + +Example 3: +Input: 2 6 0 0 5 5 0 5 5 0 0 0 0 +Output: 2 6 0 0 2 2 0 6 6 0 0 0 0 + +Below is a test input grid. Predict the corresponding output grid by applying the rule you found. Describe how you derived the rule and your overall reasoning process in detail before you submit your answer. Your final answer must be placed in tags and should be just be the text output grid itself. + +Input: +6 0 0 0 0 0 0 5 5 5 0 0 0 +Answer: 6 0 0 0 0 0 0 6 6 6 0 0 0 +Metadata: {'task_name': 'recolor_blocks_from_palette', 'size': 13, 'train_examples': [{'input': [7, 1, 0, 0, 5, 5, 0, 5, 5, 0, 0, 0, 0], 'output': [7, 1, 0, 0, 7, 7, 0, 1, 1, 0, 0, 0, 0]}, {'input': [5, 1, 0, 5, 5, 0, 5, 5, 0, 0, 0, 0, 0], 'output': [5, 1, 0, 5, 5, 0, 1, 1, 0, 0, 0, 0, 0]}, {'input': [2, 6, 0, 0, 5, 5, 0, 5, 5, 0, 0, 0, 0], 'output': [2, 6, 0, 0, 2, 2, 0, 6, 6, 0, 0, 0, 0]}], 'test_example': {'input': [6, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 0], 'output': [6, 0, 0, 0, 0, 0, 0, 6, 6, 6, 0, 0, 0]}} + +Example 2: +Question: Find the common rule that maps an input grid to an output grid, given the examples below. + +Example 1: +Input: 0 8 8 8 8 8 8 8 8 8 8 8 8 0 0 0 0 0 0 +Output: 0 0 0 0 8 8 8 8 8 8 8 8 8 8 8 8 0 0 0 + +Example 2: +Input: 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 2 0 0 0 +Output: 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 2 + +Example 3: +Input: 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 +Output: 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 + +Below is a test input grid. Predict the corresponding output grid by applying the rule you found. Describe how you derived the rule and your overall reasoning process in detail before you submit your answer. Your final answer must be placed in tags and should be just be the text output grid itself. + +Input: +0 0 0 0 0 0 6 6 6 6 6 6 6 6 6 0 0 0 0 +Answer: 0 0 0 0 0 0 0 0 0 6 6 6 6 6 6 6 6 6 0 +Metadata: {'task_name': 'move_3pix_solid', 'size': 19, 'train_examples': [{'input': [0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0], 'output': [0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0]}, {'input': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0], 'output': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2]}, {'input': [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], 'output': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0]}], 'test_example': {'input': [0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0], 'output': [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0]}} + +Example 3: +Question: Find the common rule that maps an input grid to an output grid, given the examples below. + +Example 1: +Input: 0 0 0 0 0 0 0 2 0 0 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 +Output: 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 + +Example 2: +Input: 0 0 0 2 0 0 0 0 0 0 0 0 0 3 3 3 3 3 3 3 3 0 0 0 0 0 +Output: 0 0 0 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 0 0 0 0 + +Example 3: +Input: 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 3 3 3 3 0 0 0 0 +Output: 0 0 0 0 0 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 0 0 0 + +Below is a test input grid. Predict the corresponding output grid by applying the rule you found. Describe how you derived the rule and your overall reasoning process in detail before you submit your answer. Your final answer must be placed in tags and should be just be the text output grid itself. + +Input: +0 0 0 0 0 0 0 0 0 0 0 7 7 7 7 7 7 7 7 7 7 7 7 7 2 0 +Answer: 0 0 0 0 0 0 0 0 0 0 0 7 7 7 7 7 7 7 7 7 7 7 7 7 7 0 +Metadata: {'task_name': 'block_scale_to_dot', 'size': 26, 'train_examples': [{'input': [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0], 'output': [0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0]}, {'input': [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0], 'output': [0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0]}, {'input': [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0], 'output': [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0]}], 'test_example': {'input': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 2, 0], 'output': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0]}} + +```` + +### base_conversion Generates base conversion tasks Default configuration: @@ -45,30 +214,30 @@ min_base = 2 max_base = 16 min_value = 0 max_value = 1000 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Convert the base-5 number 21e to base-3 -Answer: 21e -Metadata: {'decimal_value': 542, 'source_base': 5, 'target_base': 3, 'source_repr': '21e', 'target_repr': '21e'} +Question: Convert the base-3 number 220020 to binary +Answer: 1010001110 +Metadata: {'decimal_value': 654, 'source_base': 3, 'target_base': 2, 'source_repr': '220020', 'target_repr': '1010001110'} Example 2: -Question: Convert the base-6 number f7 to base-3 -Answer: f7 -Metadata: {'decimal_value': 247, 'source_base': 6, 'target_base': 3, 'source_repr': 'f7', 'target_repr': 'f7'} +Question: Convert the base-6 number 103 to base-13 (use lowercase letters a-z for digits above 9) +Answer: 30 +Metadata: {'decimal_value': 39, 'source_base': 6, 'target_base': 13, 'source_repr': '103', 'target_repr': '30'} Example 3: -Question: Convert the base-4 number 25d to base-14 (use lowercase letters a-z for digits above 9) -Answer: 25d -Metadata: {'decimal_value': 605, 'source_base': 4, 'target_base': 14, 'source_repr': '25d', 'target_repr': '25d'} +Question: Convert the base-10 number 418 to base-13 (use lowercase letters a-z for digits above 9) +Answer: 262 +Metadata: {'decimal_value': 418, 'source_base': 10, 'target_base': 13, 'source_repr': '418', 'target_repr': '262'} -``` +```` -### basic_arithmetic {basic-arithmetic} +### basic_arithmetic Dataset that generates basic arithmetic tasks with configurable complexity Default configuration: @@ -80,32 +249,67 @@ max_digits = 4 operators = ('+', '-', '*', '/') allow_parentheses = True allow_negation = True -seed = None +seed = 42 size = 500 format_style = simple whitespace = single ``` Example tasks: -``` +```` Example 1: -Question: 7035 / 1005 = -Answer: 7 -Metadata: {'num_terms': 2, 'num_digits': 4, 'expression': '7035 / 1005'} +Question: -5 * -6 = +Answer: 30 +Metadata: {'num_terms': 2, 'num_digits': 1, 'expression': '-5 * -6'} Example 2: -Question: -743 + 475 + 719 + -155 - -768 = -Answer: 1064 -Metadata: {'num_terms': 5, 'num_digits': 3, 'expression': '-743 + 475 + 719 + -155 - -768'} +Question: 965 / 5 = +Answer: 193 +Metadata: {'num_terms': 2, 'num_digits': 3, 'expression': '965 / 5'} Example 3: -Question: 898 / 2 + 357 + -664 * -496 = -Answer: 330150 -Metadata: {'num_terms': 5, 'num_digits': 3, 'expression': '898 / 2 + 357 + -664 * -496'} +Question: 0 + -2 + -4 * 0 * 3 = +Answer: -2 +Metadata: {'num_terms': 5, 'num_digits': 1, 'expression': '0 + -2 + -4 * 0 * 3'} + +```` +### bf +Generates BF tasks + +Default configuration: +```python +seed = 42 +size = 500 +difficulty = 1 ``` -### caesar_cipher {caesar-cipher} +Example tasks: +```` +Example 1: +Question: This is a BF (Brainf*ck) computer program. What is the output? + +>[-]>[-]<>++++++++++[<+++++++++++>-]<+.-.+++++.--------------.+++++++++++++++.< +Answer: onset +Metadata: {'bfit_code': '\nint main() {\n print("onset");\n}\n', 'bf_program': '>[-]>[-]<>++++++++++[<+++++++++++>-]<+.-.+++++.--------------.+++++++++++++++.<'} + +Example 2: +Question: This is a BF (Brainf*ck) computer program. What is the output? + +>[-]>[-]<>++++++++[<++++++++++++++>-]<.-----------.+++++++++++++.---------------.+++++.< +Answer: perch +Metadata: {'bfit_code': '\nint main() {\n print("perch");\n}\n', 'bf_program': '>[-]>[-]<>++++++++[<++++++++++++++>-]<.-----------.+++++++++++++.---------------.+++++.<'} + +Example 3: +Question: This is a BF (Brainf*ck) computer program. What is the output? + +>[-]>[-]<>+++++++++[<+++++++++++++>-]<.-------.----------.+.+++++++++++++.< +Answer: under +Metadata: {'bfit_code': '\nint main() {\n print("under");\n}\n', 'bf_program': '>[-]>[-]<>+++++++++[<+++++++++++++>-]<.-------.----------.+.+++++++++++++.<'} + +```` + +### caesar_cipher Generates Caesar cipher encryption/decryption tasks Default configuration: @@ -115,30 +319,60 @@ min_words = 3 max_words = 20 min_rotation = 1 max_rotation = 25 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Decrypt this Caesar cipher text: B EJTDVTTJPO XBT HPJOH PO XIFO IF FOUFSFE -Answer: A DISCUSSION WAS GOING ON WHEN HE ENTERED -Metadata: {'rotation': 1, 'cipher_text': 'B EJTDVTTJPO XBT HPJOH PO XIFO IF FOUFSFE', 'clear_text': 'A DISCUSSION WAS GOING ON WHEN HE ENTERED'} +Question: Decrypt this Caesar cipher text: JNJUBUF ZPVS BTTPDJBUF XIPN J XBT DPNQMJNFOUJOH B NPNFOU BHP +Answer: IMITATE YOUR ASSOCIATE WHOM I WAS COMPLIMENTING A MOMENT AGO +Metadata: {'rotation': 1, 'cipher_text': 'JNJUBUF ZPVS BTTPDJBUF XIPN J XBT DPNQMJNFOUJOH B NPNFOU BHP', 'clear_text': 'IMITATE YOUR ASSOCIATE WHOM I WAS COMPLIMENTING A MOMENT AGO'} Example 2: -Question: Decrypt this Caesar cipher text: Q IU BQZML YCQBM BQZML LW GWC VWB BPQVS BPIB I JIBP EWCTL ZMNZMAP -Answer: I AM TIRED QUITE TIRED DO YOU NOT THINK THAT A BATH WOULD REFRESH -Metadata: {'rotation': 8, 'cipher_text': 'Q IU BQZML YCQBM BQZML LW GWC VWB BPQVS BPIB I JIBP EWCTL ZMNZMAP', 'clear_text': 'I AM TIRED QUITE TIRED DO YOU NOT THINK THAT A BATH WOULD REFRESH'} +Question: Decrypt this Caesar cipher text: PBSDJ XKZYVOYX CWSDR LYEQRD SD PYB K WOBO KXN YBSQSXKDON DOVOZRYXSM TYEBXKVSCW +Answer: FRITZ NAPOLEON SMITH BOUGHT IT FOR A MERE AND ORIGINATED TELEPHONIC JOURNALISM +Metadata: {'rotation': 10, 'cipher_text': 'PBSDJ XKZYVOYX CWSDR LYEQRD SD PYB K WOBO KXN YBSQSXKDON DOVOZRYXSM TYEBXKVSCW', 'clear_text': 'FRITZ NAPOLEON SMITH BOUGHT IT FOR A MERE AND ORIGINATED TELEPHONIC JOURNALISM'} Example 3: -Question: Decrypt this Caesar cipher text: Y IGLE GQ FC -Answer: A KING IS HE -Metadata: {'rotation': 24, 'cipher_text': 'Y IGLE GQ FC', 'clear_text': 'A KING IS HE'} +Question: Decrypt this Caesar cipher text: ZW PFLI JKFDRTY ZJ FLK FW ZK DLJK SV DVEUVU +Answer: IF YOUR STOMACH IS OUT OF IT MUST BE MENDED +Metadata: {'rotation': 17, 'cipher_text': 'ZW PFLI JKFDRTY ZJ FLK FW ZK DLJK SV DVEUVU', 'clear_text': 'IF YOUR STOMACH IS OUT OF IT MUST BE MENDED'} +```` + +### calendar_arithmetic +Default configuration: +```python +year = 2022 +tasks = ['weekday_offset', 'weekday_of_date', 'weekday_of_date_from_first_day', 'recurring_event_day', 'count_days', 'count_business_days', 'is_leap_year'] +offset_upper_bound = 100 +leap_year_range = 200 +seed = 42 +size = 500 ``` -### chain_sum {chain-sum} +Example tasks: +```` +Example 1: +Question: Between Sunday, February 27, 2022 and Wednesday, March 2, 2022 (counting both dates), what's the total count of business days (Monday through Friday)? Give the count numerically. +Answer: 3 +Metadata: {'task': 'count_business_days', 'start_date': '2022-02-27', 'end_date': '2022-03-02'} + +Example 2: +Question: Starting from Monday, May 23, 2022, which weekday was it 98 days before? Write out the full weekday name. +Answer: Monday +Metadata: {'task': 'weekday_offset', 'start_date': '2022-05-23', 'offset_days': -98, 'target_date': '2022-02-14'} + +Example 3: +Question: If a meeting is scheduled on the last Saturday of September 2022, on which day of the month does it occur? Respond with just the number. Answer with -1 if the ordinal does not exist in the month. +Answer: 24 +Metadata: {'task': 'recurring_event_day', 'year': 2022, 'month': 9, 'ordinal': 'last', 'weekday': 'Saturday'} + +```` + +### chain_sum Generates simple arithmetic tasks using only + and - operators Default configuration: @@ -148,94 +382,98 @@ max_terms = 6 min_digits = 1 max_digits = 4 allow_negation = False -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: 47 - 18 - 85 + 54 = -Answer: -2 -Metadata: {'num_terms': 4, 'num_digits': 2, 'expression': '47 - 18 - 85 + 54'} +Question: 4 + 3 = +Answer: 7 +Metadata: {'num_terms': 2, 'num_digits': 1, 'expression': '4 + 3'} Example 2: -Question: 52 + 23 - 88 + 78 + 92 - 54 = -Answer: 103 -Metadata: {'num_terms': 6, 'num_digits': 2, 'expression': '52 + 23 - 88 + 78 + 92 - 54'} +Question: 812 + 880 = +Answer: 1692 +Metadata: {'num_terms': 2, 'num_digits': 3, 'expression': '812 + 880'} Example 3: -Question: 46 + 76 + 75 + 46 + 70 - 88 = -Answer: 225 -Metadata: {'num_terms': 6, 'num_digits': 2, 'expression': '46 + 76 + 75 + 46 + 70 - 88'} +Question: 2 + 6 + 3 + 4 + 0 = +Answer: 15 +Metadata: {'num_terms': 5, 'num_digits': 1, 'expression': '2 + 6 + 3 + 4 + 0'} -``` +```` -### color_cube_rotation {color-cube-rotation} +### color_cube_rotation Generates color cube rotation reasoning tasks Default configuration: ```python min_rotations = 1 max_rotations = 3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: A cube has: -- a white top side -- a silver right side -- a brown front side -- a violet left side -- a pink back side -- a purple bottom side +- a pink top side +- a gray right side +- a orange front side +- a purple left side +- a indigo back side +- a cyan bottom side -The cube is rotated so that the side which was before at the left is now at the top. - -Then the cube is rotated to bring the bottom side to the top. +The cube is rotated so that the side which was before at the bottom is now at the top. -Now the cube is rotated to place its back side at the top. - -What is now the color of the top side of the cube? -Answer: brown -Metadata: {'initial_state': {'top': 'white', 'right': 'silver', 'front': 'brown', 'left': 'violet', 'back': 'pink', 'bottom': 'purple'}, 'rotations': ['left', 'bottom', 'back'], 'target_side': 'top', 'num_rotations': 3} +What is now the color of the back side of the cube? +Answer: orange +Metadata: {'initial_state': {'top': 'pink', 'right': 'gray', 'front': 'orange', 'left': 'purple', 'back': 'indigo', 'bottom': 'cyan'}, 'rotations': ['bottom'], 'target_side': 'back', 'num_rotations': 1} Example 2: Question: A cube has: -- a violet top side -- a green right side -- a white front side -- a blue left side -- a gold back side +- a gray top side +- a brown right side +- a silver front side +- a red left side +- a purple back side - a yellow bottom side -The cube is rotated so that the side which was before at the right is now at the top. +The cube is rotated so that the side which was before at the left is now at the top. + +Next, the bottom side is rotated to become the top face. + +After that the cube is turned to make the bottom face the top. -What is now the color of the bottom side of the cube? -Answer: blue -Metadata: {'initial_state': {'top': 'violet', 'right': 'green', 'front': 'white', 'left': 'blue', 'back': 'gold', 'bottom': 'yellow'}, 'rotations': ['right'], 'target_side': 'bottom', 'num_rotations': 1} +What is now the color of the left side of the cube? +Answer: yellow +Metadata: {'initial_state': {'top': 'gray', 'right': 'brown', 'front': 'silver', 'left': 'red', 'back': 'purple', 'bottom': 'yellow'}, 'rotations': ['left', 'bottom', 'bottom'], 'target_side': 'left', 'num_rotations': 3} Example 3: Question: A cube has: -- a magenta top side -- a green right side -- a brown front side -- a yellow left side -- a silver back side -- a violet bottom side +- a orange top side +- a cyan right side +- a violet front side +- a pink left side +- a gray back side +- a gold bottom side -The cube is rotated so that the side which was before at the front is now at the top. +The cube is rotated so that the side which was before at the left is now at the top. -What is now the color of the bottom side of the cube? -Answer: silver -Metadata: {'initial_state': {'top': 'magenta', 'right': 'green', 'front': 'brown', 'left': 'yellow', 'back': 'silver', 'bottom': 'violet'}, 'rotations': ['front'], 'target_side': 'bottom', 'num_rotations': 1} +Now the cube is rotated to place its back side at the top. -``` +Now the cube is rotated to place its bottom side at the top. -### countdown {countdown} +What is now the color of the left side of the cube? +Answer: gold +Metadata: {'initial_state': {'top': 'orange', 'right': 'cyan', 'front': 'violet', 'left': 'pink', 'back': 'gray', 'bottom': 'gold'}, 'rotations': ['left', 'back', 'bottom'], 'target_side': 'left', 'num_rotations': 3} + +```` + +### countdown Generates Countdown Number Game tasks Default configuration: @@ -248,33 +486,33 @@ min_target = 100 max_target = 999 operators = ('+', '-', '*', '/') shuffle = True -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Using the numbers 4, 15, 90, 49, 15, create an expression that equals 275. -You can only use each number once. -Answer: 49*90/15 - 15 - 4 -Metadata: {'numbers': [4, 15, 90, 49, 15], 'target': 275, 'expression': '49*90/15 - 15 - 4'} +Question: Calculate 139 using the numbers 36, 29, 95, 32, 4, 15. +Each number may be used at most once. +Answer: 15 - 4 + 95 + 36 - 32 + 29 +Metadata: {'numbers': [36, 29, 95, 32, 4, 15], 'target': 139, 'expression': '15 - 4 + 95 + 36 - 32 + 29'} Example 2: -Question: Calculate 237 using the numbers 32, 56, 64, 23, 3, 100. -Each number may be used at most once. -Answer: 100*3 - 64 - 23 - 32 + 56 -Metadata: {'numbers': [32, 56, 64, 23, 3, 100], 'target': 237, 'expression': '100*3 - 64 - 23 - 32 + 56'} +Question: Using the numbers 74, 48, 56, 66, create an expression that equals 132. +You can only use each number once. +Answer: 66 - 56 + 74 + 48 +Metadata: {'numbers': [74, 48, 56, 66], 'target': 132, 'expression': '66 - 56 + 74 + 48'} Example 3: -Question: Find a way to make 241 using some or all of these numbers: 87, 85, 82, 13. -Each number can only be used once. -Answer: 85 + 82 - 13 + 87 -Metadata: {'numbers': [87, 85, 82, 13], 'target': 241, 'expression': '85 + 82 - 13 + 87'} +Question: Using the numbers 5, 41, 38, 81, 14, create an expression that equals 450. +You can only use each number once. +Answer: 41*14 - 81 - 38 - 5 +Metadata: {'numbers': [5, 41, 38, 81, 14], 'target': 450, 'expression': '41*14 - 81 - 38 - 5'} -``` +```` -### family_relationships {family-relationships} +### family_relationships Generates family relationship reasoning tasks Default configuration: @@ -283,36 +521,36 @@ min_family_size = 4 max_family_size = 8 male_names = ['James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles', 'Peter', 'Daniel', 'Matthew', 'Christopher', 'Andrew', 'George', 'Edward', 'Benjamin', 'Henry', 'Samuel', 'Alexander', 'Oliver', 'Jack', 'Harry', 'Jacob', 'Noah', 'Ethan', 'Lucas', 'Mason', 'Logan', 'Sebastian', 'Theodore', 'Owen', 'Liam', 'Aiden', 'Kai', 'Jayden', 'Zion', 'Phoenix', 'Atlas', 'Axel', 'Ryder', 'Finn'] female_names = ['Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Emma', 'Lisa', 'Anna', 'Margaret', 'Victoria', 'Charlotte', 'Sophia', 'Isabella', 'Olivia', 'Ava', 'Mia', 'Emily', 'Abigail', 'Amelia', 'Eleanor', 'Grace', 'Alice', 'Lucy', 'Chloe', 'Sophie', 'Lily', 'Hannah', 'Zoe', 'Luna', 'Nova', 'Aria', 'Willow', 'Aurora', 'Sage', 'River', 'Winter', 'Sky', 'Rain'] -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Charles is married to Jessica. They have a child called Atlas. Atlas is married to Amelia. They have children called Patricia and Nova. David is married to Ava. They have a child called Amelia. +Question: John is married to Isabella. They have a child called Edward. Edward is married to Victoria. -How is Jessica related to Nova? -Answer: grandmother -Metadata: {'person1': 'Jessica', 'person2': 'Nova', 'relationship': 'grandmother', 'family_size': 8} +What is Isabella to Edward? +Answer: mother +Metadata: {'person1': 'Isabella', 'person2': 'Edward', 'relationship': 'mother', 'family_size': 4} Example 2: -Question: David is married to Charlotte. They have a child called Lucas. Lucas is married to Victoria. They have children called James and Abigail. +Question: Henry is married to Karen. They have a child called Sebastian. Sebastian is married to Eleanor. -What is Victoria to Abigail? -Answer: mother -Metadata: {'person1': 'Victoria', 'person2': 'Abigail', 'relationship': 'mother', 'family_size': 6} +What relation is Henry to Karen? +Answer: husband +Metadata: {'person1': 'Henry', 'person2': 'Karen', 'relationship': 'husband', 'family_size': 4} Example 3: -Question: Mason is married to Amelia. They have a child called James. James is married to Grace. They have a child called Abigail. +Question: Liam is married to Nova. They have a child called Noah. Noah is married to Charlotte. They have a child called Patricia. Joseph is married to Lisa. They have a child called Charlotte. -What relation is James to Amelia? -Answer: son -Metadata: {'person1': 'James', 'person2': 'Amelia', 'relationship': 'son', 'family_size': 5} +What is Liam to Noah? +Answer: father +Metadata: {'person1': 'Liam', 'person2': 'Noah', 'relationship': 'father', 'family_size': 7} -``` +```` -### figlet_font {figlet-font} +### figlet_font Generates FigletFont tasks Default configuration: @@ -320,51 +558,63 @@ Default configuration: static_word = None static_font = None space_letters = True -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: What word does this say? +Question: Please read the following figlet font: - __ _ -(_ |_| / \ | | |\| -__) | | \_/ |^| | | + sSSSs d s b sss. d sss sss sssss + S S S S S d S S +S S S SS Y S S +S S S S ss. S sSSs S +S S S S b S S + S S S S P S S + "sss" P P ` ss' P sSSss P + -Answer: SHOWN -Metadata: {'font': 'bigfig', 'space_letters': True} +Answer: ONSET +Metadata: {'font': 'amc_tubes', 'space_letters': True} Example 2: Question: What word does this say? - ##### ### ### ## ###### ###### - ## ### ## ## ##### ### ## ###### -## ## ## ## ### ## ## ## -## ####### ## ## ## ## ## -## ## ## ####### ##### ## -##### ## ## ## ## ## ## ## - ##### # # # # ## ## # - +###### ###### ###### #### ## ## + ## ## ## ## ## ## ## ## ## ## + ## ## ## ## ## ## ## ## ## + ##### #### ##### ## ###### + ## ## ## ## ## ## ## ## + ## ## ## ## ## ## ## ## ## +#### ###### ### ### #### ## ## + -Answer: CHART -Metadata: {'font': 'future_6', 'space_letters': True} +Answer: PERCH +Metadata: {'font': 'demo_2__', 'space_letters': True} Example 3: -Question: Please read the following figlet font: +Question: What word does this say? -.dP"Y8 88 88 888888 db 88""Yb -`Ybo." 88 88 88__ dPYb 88__dP -o.`Y8b 888888 88"" dP__Yb 88"Yb -8bodP' 88 88 888888 dP""""Yb 88 Yb + + + +### ### ### ### ##### ###### ##### + ## ## ## # ## ## ## # ## ## + ## ## ### # ## ## #### ## ## + ## ## ##### ## ## ## #### + ## ## ## ## ## ## ## ## ## ## + ### ### ## ##### ###### #### ## + + -Answer: SHEAR -Metadata: {'font': '4max', 'space_letters': True} +Answer: UNDER +Metadata: {'font': 'xcourb', 'space_letters': True} -``` +```` -### fraction_simplification {fraction-simplification} +### fraction_simplification Generates fraction simplification tasks Default configuration: @@ -374,30 +624,182 @@ max_value = 1000 min_factor = 1 max_factor = 100 styles = ('plain', 'latex_inline', 'latex_frac', 'latex_dfrac') -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Simplify the fraction $12054/36848$ to its lowest terms -Answer: $123/376$ -Metadata: {'numerator': 12054, 'denominator': 36848, 'simplified_numerator': 123, 'simplified_denominator': 376, 'reduction_factor': 98, 'style': 'latex_inline'} +Question: Simplify the fraction $\frac{92}{524}$ to its lowest terms +Answer: $\frac{23}{131}$ +Metadata: {'numerator': 92, 'denominator': 524, 'simplified_numerator': 23, 'simplified_denominator': 131, 'reduction_factor': 4, 'style': 'latex_frac'} Example 2: -Question: Simplify the fraction 1218/28275 to its lowest terms -Answer: 14/325 -Metadata: {'numerator': 1218, 'denominator': 28275, 'simplified_numerator': 14, 'simplified_denominator': 325, 'reduction_factor': 87, 'style': 'plain'} +Question: Simplify the fraction $3600/26370$ to its lowest terms +Answer: $40/293$ +Metadata: {'numerator': 3600, 'denominator': 26370, 'simplified_numerator': 40, 'simplified_denominator': 293, 'reduction_factor': 90, 'style': 'latex_inline'} Example 3: -Question: Simplify the fraction 21902/24111 to its lowest terms -Answer: 466/513 -Metadata: {'numerator': 21902, 'denominator': 24111, 'simplified_numerator': 466, 'simplified_denominator': 513, 'reduction_factor': 47, 'style': 'plain'} +Question: Simplify the fraction 29330/37310 to its lowest terms +Answer: 419/533 +Metadata: {'numerator': 29330, 'denominator': 37310, 'simplified_numerator': 419, 'simplified_denominator': 533, 'reduction_factor': 70, 'style': 'plain'} + +```` + +### game_of_life +Generates Game of Life games with configurable parameters +Default configuration: +```python +grid_size_x = 20 +grid_size_y = 20 +filled_cells = 100 +simulation_steps = 1 +seed = 42 +size = 500 ``` -### gcd {gcd} +Example tasks: +```` +Example 1: +Question: What will this Game of Life board look like after 1 steps of simulation? + +[[0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0] + [0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 1 0] + [1 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 1 0 0] + [0 0 0 0 1 1 1 0 0 0 0 1 0 0 0 0 1 0 0 0] + [0 0 0 0 0 0 0 0 1 0 0 1 0 1 1 0 0 1 0 0] + [0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 0] + [1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0] + [1 1 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0] + [0 0 1 0 0 0 0 1 0 0 0 0 1 1 0 0 1 0 0 1] + [1 1 0 1 0 0 1 0 1 0 0 0 1 0 0 0 0 0 0 0] + [0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 1 1] + [0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1] + [0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1] + [0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 1 1 1 0] + [1 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0] + [1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 1] + [0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0] + [0 0 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0] + [0 0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0]] +Answer: [[0 0 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0] + [0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 0] + [0 0 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 1 0] + [0 0 0 0 0 1 1 1 0 0 1 1 0 1 0 0 1 1 0 0] + [0 0 0 0 0 1 1 1 0 0 0 0 1 0 0 0 1 1 1 0] + [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1] + [1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0] + [0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0] + [0 0 0 0 0 0 1 1 1 0 0 0 1 1 0 0 0 0 0 1] + [1 1 1 0 0 0 1 0 1 0 0 0 1 0 0 0 0 0 1 0] + [0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 1 1] + [0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1] + [1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1] + [1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1] + [1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0] + [1 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1] + [0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0] + [0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]] +Metadata: {'grid_size_x': 20, 'grid_size_y': 20, 'filled_cells': 100, 'simulation_steps': 1} + +Example 2: +Question: What will this Game of Life board look like after 1 steps of simulation? + +[[1 0 0 1 0 1 1 0 0 0 1 0 0 0 0 0 1 0 0 0] + [0 0 1 1 1 1 0 0 0 1 0 0 0 0 0 1 0 0 1 0] + [0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 0 0 0] + [0 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1 1 1] + [0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0] + [0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0] + [1 1 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0] + [0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0] + [0 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 1 0 0 0] + [0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1] + [0 0 1 1 1 1 0 0 1 0 0 1 1 0 0 0 0 0 0 1] + [0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 1] + [0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 1 0 1 1] + [0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1] + [0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0] + [0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1] + [0 1 0 0 1 1 0 0 1 0 0 1 0 0 0 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0]] +Answer: [[0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1] + [0 0 1 1 0 1 1 0 0 1 1 0 0 0 0 1 0 1 0 0] + [0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 1 1 0 0 1] + [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1] + [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1] + [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0] + [1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [1 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0] + [0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [1 0 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 0 0 0] + [1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 1] + [1 0 1 1 0 1 0 0 0 1 1 0 0 0 0 0 0 1 0 0] + [1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0] + [0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 1] + [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0] + [0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0]] +Metadata: {'grid_size_x': 20, 'grid_size_y': 20, 'filled_cells': 100, 'simulation_steps': 1} + +Example 3: +Question: What will this Game of Life board look like after 1 steps of simulation? + +[[0 0 1 1 0 0 0 1 0 0 1 0 0 1 0 0 0 0 1 1] + [0 0 0 0 0 0 0 0 1 1 1 1 0 1 0 0 0 0 0 1] + [0 0 0 1 0 0 0 0 1 1 1 0 0 0 0 0 1 0 0 0] + [0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0] + [0 0 1 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 1 1 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0] + [0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0] + [0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 1] + [0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0] + [1 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1] + [0 0 0 1 0 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0] + [0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0] + [0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0] + [0 0 1 0 0 0 0 0 1 0 0 1 1 0 0 0 0 0 1 0] + [0 0 1 1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0] + [0 0 1 1 0 0 1 0 1 0 0 1 0 0 1 0 0 0 0 0] + [1 0 0 1 1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 0] + [0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0] + [0 1 0 0 1 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0]] +Answer: [[1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1] + [0 0 1 1 0 0 0 1 0 0 0 1 1 0 0 0 0 0 1 1] + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 1 1 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0] + [0 0 0 0 1 1 1 1 1 0 0 1 1 0 0 0 0 0 0 0] + [0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0] + [0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0] + [0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0] + [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1] + [0 1 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0] + [0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1] + [0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0] + [0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0] + [0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 1 0 0] + [0 0 1 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0] + [0 1 0 0 1 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0] + [0 1 0 0 0 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0] + [0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0] + [0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 1 0 0 0] + [0 0 0 0 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 0]] +Metadata: {'grid_size_x': 20, 'grid_size_y': 20, 'filled_cells': 100, 'simulation_steps': 1} + +```` + +### gcd Generates Greatest Common Divisor (GCD) tasks Default configuration: @@ -406,30 +808,72 @@ min_numbers = 2 max_numbers = 2 min_value = 1 max_value = 1000 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Find the Greatest Common Divisor (GCD) of these numbers: 384, 414 -Answer: 6 -Metadata: {'numbers': [384, 414], 'result': 6} +Question: Find the Greatest Common Divisor (GCD) of these numbers: 26, 760 +Answer: 2 +Metadata: {'numbers': [26, 760], 'result': 2} Example 2: -Question: Find the Greatest Common Divisor (GCD) of these numbers: 298, 803 -Answer: 1 -Metadata: {'numbers': [298, 803], 'result': 1} +Question: Find the Greatest Common Divisor (GCD) of these numbers: 688, 716 +Answer: 4 +Metadata: {'numbers': [688, 716], 'result': 4} Example 3: -Question: Find the Greatest Common Divisor (GCD) of these numbers: 846, 550 -Answer: 2 -Metadata: {'numbers': [846, 550], 'result': 2} +Question: Find the Greatest Common Divisor (GCD) of these numbers: 297, 30 +Answer: 3 +Metadata: {'numbers': [297, 30], 'result': 3} + +```` + +### intermediate_integration +Generates intermediate integration problem - either + by substitution or by parts +Default configuration: +```python +problem_types = ('substitution', 'by_parts') +substitution_types = ('linear', 'trigonometric', 'exponential', 'radical') +by_parts_types = ('polynomial_exp_trig', 'log_inverse_trig', 'cyclic', 'repeated_parts') +seed = 42 +size = 500 +linear_lower_bound = 1 +linear_upper_bound = 10 +min_linear_degree = 2 +max_linear_degree = 4 +outer_constant_min = 1 +outer_constant_max = 3 +min_poly_degree = 1 +max_poly_degree = 3 +symbols = ('x', 'X') +operators = ('+', '-') ``` -### lcm {lcm} +Example tasks: +```` +Example 1: +Question: Find the indefinite integral: ∫ -3*exp(3*x + 9) dx +Answer: -exp(3*x + 9) + C +Metadata: {'integrand': '-3*exp(3*x + 9)', 'problem_type': 'substitution', 'variable': 'x', 'type': 'exponential', 'expected_answer_expression': -exp(3*x + 9)} + +Example 2: +Question: Evaluate the indefinite integral: ∫ -6*sin(2*X + 10)*cos(2*X + 10)**4 dx +Answer: 3*cos(2*X + 10)**5/5 + C +Metadata: {'integrand': '-6*sin(2*X + 10)*cos(2*X + 10)**4', 'problem_type': 'substitution', 'variable': 'X', 'type': 'trigonometric', 'expected_answer_expression': 3*cos(2*X + 10)**5/5} + +Example 3: +Question: Find the indefinite integral: ∫ 2*asin(x) dx +Answer: 2*Integral(asin(x), x) + C +Metadata: {'integrand': '2*asin(x)', 'problem_type': 'by_parts', 'variable': 'x', 'type': 'log_inverse_trig', 'expected_answer_expression': 2*Integral(asin(x), x)} + +```` + +### lcm Generates Least Common Multiple (LCM) tasks Default configuration: @@ -438,30 +882,30 @@ min_numbers = 2 max_numbers = 2 min_value = 1 max_value = 100 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Find the Least Common Multiple (LCM) of these numbers: 33, 84 -Answer: 924 -Metadata: {'numbers': [33, 84], 'result': 924} +Question: Find the Least Common Multiple (LCM) of these numbers: 95, 14 +Answer: 1330 +Metadata: {'numbers': [95, 14], 'result': 1330} Example 2: -Question: Find the Least Common Multiple (LCM) of these numbers: 16, 23 -Answer: 368 -Metadata: {'numbers': [16, 23], 'result': 368} +Question: Find the Least Common Multiple (LCM) of these numbers: 60, 48 +Answer: 240 +Metadata: {'numbers': [60, 48], 'result': 240} Example 3: -Question: Find the Least Common Multiple (LCM) of these numbers: 66, 88 -Answer: 264 -Metadata: {'numbers': [66, 88], 'result': 264} +Question: Find the Least Common Multiple (LCM) of these numbers: 38, 4 +Answer: 76 +Metadata: {'numbers': [38, 4], 'result': 76} -``` +```` -### leg_counting {leg-counting} +### leg_counting Generates leg counting arithmetic tasks Default configuration: @@ -469,60 +913,60 @@ Default configuration: min_animals = 2 max_animals = 5 max_instances = 3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: How many legs are there in total if you have 2 scorpions, 3 sea slugs, 2 cockroachs, 2 fireflys? -Answer: 40 -Metadata: {'animals': {'scorpion': 2, 'sea slug': 3, 'cockroach': 2, 'firefly': 2}, 'total_legs': 40} +Question: How many legs are there in total if you have 1 sea slug, 1 deer? +Answer: 4 +Metadata: {'animals': {'sea slug': 1, 'deer': 1}, 'total_legs': 4} Example 2: -Question: How many legs are there in total if you have 2 shrimps, 2 deers? -Answer: 28 -Metadata: {'animals': {'shrimp': 2, 'deer': 2}, 'total_legs': 28} +Question: How many legs are there in total if you have 2 sheeps, 2 dogs? +Answer: 16 +Metadata: {'animals': {'sheep': 2, 'dog': 2}, 'total_legs': 16} Example 3: -Question: How many legs are there in total if you have 1 beetle, 3 spiders, 1 jellyfish? -Answer: 30 -Metadata: {'animals': {'beetle': 1, 'spider': 3, 'jellyfish': 1}, 'total_legs': 30} +Question: How many legs are there in total if you have 1 crab, 2 lobsters, 1 human, 1 cow, 1 bee? +Answer: 42 +Metadata: {'animals': {'crab': 1, 'lobster': 2, 'human': 1, 'cow': 1, 'bee': 1}, 'total_legs': 42} -``` +```` -### letter_counting {letter-counting} +### letter_counting Generates letter counting tasks from text spans Default configuration: ```python min_words = 5 max_words = 15 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: How many times does the letter "s" appear in the text: "a varied assortment is always in readiness A subscription"? -Answer: 8 -Metadata: {'span_length': 9, 'target_letter': 's', 'span': ['a', 'varied', 'assortment', 'is', 'always', 'in', 'readiness', 'A', 'subscription']} +Question: How many times does the letter "a" appear in the text: "bed and enters his mechanical dresser Two minutes later the machine deposited him all dressed"? +Answer: 6 +Metadata: {'span_length': 15, 'target_letter': 'a', 'span': ['bed', 'and', 'enters', 'his', 'mechanical', 'dresser', 'Two', 'minutes', 'later', 'the', 'machine', 'deposited', 'him', 'all', 'dressed']} Example 2: -Question: How many times does the letter "c" appear in the text: "exclaims every one present Yes answers"? +Question: How many times does the letter "w" appear in the text: "it into a watering place"? Answer: 1 -Metadata: {'span_length': 6, 'target_letter': 'c', 'span': ['exclaims', 'every', 'one', 'present', 'Yes', 'answers']} +Metadata: {'span_length': 5, 'target_letter': 'w', 'span': ['it', 'into', 'a', 'watering', 'place']} Example 3: -Question: How many times does the letter "f" appear in the text: "individual Project Gutenberg electronic work is derived from texts"? -Answer: 1 -Metadata: {'span_length': 9, 'target_letter': 'f', 'span': ['individual', 'Project', 'Gutenberg', 'electronic', 'work', 'is', 'derived', 'from', 'texts']} +Question: How many times does the letter "t" appear in the text: "readable form accessible by the widest array of equipment including outdated"? +Answer: 5 +Metadata: {'span_length': 11, 'target_letter': 't', 'span': ['readable', 'form', 'accessible', 'by', 'the', 'widest', 'array', 'of', 'equipment', 'including', 'outdated']} -``` +```` -### letter_jumble {letter-jumble} +### letter_jumble Generates word letter jumbling tasks Default configuration: @@ -534,30 +978,30 @@ max_words = 20 min_corruption_level = 0.1 max_corruption_level = 0.9 consecutive_words = True -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Unscramble these words: Acemira bnleogs ot -Answer: America belongs to -Metadata: {'num_words': 3, 'corruption_level': 0.3687132105849005, 'scrambled_words': ['Acemira', 'bnleogs', 'ot'], 'original_words': ['America', 'belongs', 'to']} +Question: Unscramble these words: ew hsall eb ebla ot puodrce +Answer: we shall be able to produce +Metadata: {'num_words': 6, 'corruption_level': 0.12000860417813355, 'scrambled_words': ['ew', 'hsall', 'eb', 'ebla', 'ot', 'puodrce'], 'original_words': ['we', 'shall', 'be', 'able', 'to', 'produce']} Example 2: -Question: Unscramble these words: cubssribres ton noly -Answer: subscribers not only -Metadata: {'num_words': 3, 'corruption_level': 0.38741746634525664, 'scrambled_words': ['cubssribres', 'ton', 'noly'], 'original_words': ['subscribers', 'not', 'only']} +Question: Unscramble these words: ni oiurnalmsj Well Cahs +Answer: in journalism Well Cash +Metadata: {'num_words': 4, 'corruption_level': 0.3288673442377109, 'scrambled_words': ['ni', 'oiurnalmsj', 'Well', 'Cahs'], 'original_words': ['in', 'journalism', 'Well', 'Cash']} Example 3: -Question: Unscramble these words: yuo peerntd ttha yuo exepct ot cantmafuure a mnhau iegnb uot adn uot Wyh ton rM itSmh cndedaav -Answer: you pretend that you expect to manufacture a human being out and out Why not Mr Smith advanced -Metadata: {'num_words': 18, 'corruption_level': 0.5094277166629008, 'scrambled_words': ['yuo', 'peerntd', 'ttha', 'yuo', 'exepct', 'ot', 'cantmafuure', 'a', 'mnhau', 'iegnb', 'uot', 'adn', 'uot', 'Wyh', 'ton', 'rM', 'itSmh', 'cndedaav'], 'original_words': ['you', 'pretend', 'that', 'you', 'expect', 'to', 'manufacture', 'a', 'human', 'being', 'out', 'and', 'out', 'Why', 'not', 'Mr', 'Smith', 'advanced']} +Question: Unscramble these words: dear rchAdbali keep no nSice yrstyedae atnhks ot oyu rheet si a gain fo sucrbbisesr rM +Answer: dear Archibald keep on Since yesterday thanks to you there is a gain of subscribers Mr +Metadata: {'num_words': 16, 'corruption_level': 0.516016391169858, 'scrambled_words': ['dear', 'rchAdbali', 'keep', 'no', 'nSice', 'yrstyedae', 'atnhks', 'ot', 'oyu', 'rheet', 'si', 'a', 'gain', 'fo', 'sucrbbisesr', 'rM'], 'original_words': ['dear', 'Archibald', 'keep', 'on', 'Since', 'yesterday', 'thanks', 'to', 'you', 'there', 'is', 'a', 'gain', 'of', 'subscribers', 'Mr']} -``` +```` -### maze {maze} +### maze Generates mazes with guaranteed shortest path distance from start to goal within [min_dist, max_dist]. @@ -567,121 +1011,219 @@ min_dist = 5 max_dist = 10 min_grid_size = 5 max_grid_size = 10 -seed = None +seed = 42 size = 50 ``` Example tasks: -``` +```` Example 1: -Question: Navigate from 'a' (start) to ':' (goal): - -```xxxxxxxxxx -xxxx?xx:xx -xxxx??x??x -xx????x??x -xxx?x???xx -x?x?????xx -x??ax???xx -x???xxx??x -x????x?xxx -xxxxxxxxxx``` -Legend: 'x' = Wall, '?' = Passage +Question: Navigate from '3' (start) to 'z' (goal): + +``` +>>>>>>>>> +>eeee>e>> +>ee>>>>>> +>eeeeee>> +>e>ee>>e> +>>ez>3e>> +>eee>e>e> +>eeeee>e> +>>>>>>>>> +``` +Legend: '>' = Wall, 'e' = Passage What is the minimum number of steps to reach the goal? -Answer: 9 -Metadata: {'grid_size': 10, 'grid': ['xxxxxxxxxx', 'xxxx?xx:xx', 'xxxx??x??x', 'xx????x??x', 'xxx?x???xx', 'x?x?????xx', 'x??ax???xx', 'x???xxx??x', 'x????x?xxx', 'xxxxxxxxxx'], 'shortest_path_length': 9, 'start': 'a', 'goal': ':', 'wall': 'x', 'path': '?'} +Answer: 6 +Metadata: {'grid_size': 9, 'grid': ['>>>>>>>>>', '>eeee>e>>', '>ee>>>>>>', '>eeeeee>>', '>e>ee>>e>', '>>ez>3e>>', '>eee>e>e>', '>eeeee>e>', '>>>>>>>>>'], 'shortest_path_length': 6, 'start': '3', 'goal': 'z', 'wall': '>', 'path': 'e'} Example 2: -Question: Navigate from '"' (start) to '}' (goal): - -```444444444 -4##4#4##4 -44}444444 -44##4#444 -4#####"44 -4##4####4 -444#####4 -4##4#4444 -444444444``` -Legend: '4' = Wall, '#' = Passage +Question: Navigate from '`' (start) to 'i' (goal): + +``` +4444444 +4AAAAi4 +4A4A4A4 +4A4AA44 +44AAAA4 +44A`444 +4444444 +``` +Legend: '4' = Wall, 'A' = Passage What is the minimum number of steps to reach the goal? Answer: 6 -Metadata: {'grid_size': 9, 'grid': ['444444444', '4##4#4##4', '44}444444', '44##4#444', '4#####"44', '4##4####4', '444#####4', '4##4#4444', '444444444'], 'shortest_path_length': 6, 'start': '"', 'goal': '}', 'wall': '4', 'path': '#'} +Metadata: {'grid_size': 7, 'grid': ['4444444', '4AAAAi4', '4A4A4A4', '4A4AA44', '44AAAA4', '44A`444', '4444444'], 'shortest_path_length': 6, 'start': '`', 'goal': 'i', 'wall': '4', 'path': 'A'} Example 3: -Question: Navigate from '(' (start) to '$' (goal): - -```eeeeeeeee -e(%%%%%ee -e%%%%%eee -ee%eee%ee -e%%%%%$%e -e%%%%e%ee -e%%%%%%%e -ee%%%e%%e -eeeeeeeee``` -Legend: 'e' = Wall, '%' = Passage +Question: Navigate from '(' (start) to '`' (goal): + +``` +QQQQQQQ +QQ%%%%Q +QQ`%Q%Q +Q%%Q%%Q +Q%%%Q%Q +Q%QQ%(Q +QQQQQQQ +``` +Legend: 'Q' = Wall, '%' = Passage What is the minimum number of steps to reach the goal? Answer: 8 -Metadata: {'grid_size': 9, 'grid': ['eeeeeeeee', 'e(%%%%%ee', 'e%%%%%eee', 'ee%eee%ee', 'e%%%%%$%e', 'e%%%%e%ee', 'e%%%%%%%e', 'ee%%%e%%e', 'eeeeeeeee'], 'shortest_path_length': 8, 'start': '(', 'goal': '$', 'wall': 'e', 'path': '%'} +Metadata: {'grid_size': 7, 'grid': ['QQQQQQQ', 'QQ%%%%Q', 'QQ`%Q%Q', 'Q%%Q%%Q', 'Q%%%Q%Q', 'Q%QQ%(Q', 'QQQQQQQ'], 'shortest_path_length': 8, 'start': '(', 'goal': '`', 'wall': 'Q', 'path': '%'} -``` +```` -### mini_sudoku {mini-sudoku} +### mini_sudoku Generates 4x4 sudoku puzzles with configurable difficulty Default configuration: ```python min_empty = 8 max_empty = 12 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: Solve this 4x4 Mini Sudoku puzzle: -_ 3 _ 1 -2 1 _ _ -_ _ _ 2 -3 2 _ 4 -Answer: 4 3 2 1 -2 1 4 3 -1 4 3 2 -3 2 1 4 -Metadata: {'puzzle': [[0, 3, 0, 1], [2, 1, 0, 0], [0, 0, 0, 2], [3, 2, 0, 4]], 'solution': [[4, 3, 2, 1], [2, 1, 4, 3], [1, 4, 3, 2], [3, 2, 1, 4]], 'num_empty': 8} +_ _ _ _ +_ _ _ _ +_ 1 3 _ +_ 4 _ 1 +Answer: 4 2 1 3 +1 3 4 2 +2 1 3 4 +3 4 2 1 +Metadata: {'puzzle': [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 3, 0], [0, 4, 0, 1]], 'solution': [[4, 2, 1, 3], [1, 3, 4, 2], [2, 1, 3, 4], [3, 4, 2, 1]], 'num_empty': 12} Example 2: Question: Solve this 4x4 Mini Sudoku puzzle: -1 _ _ _ -_ _ 1 _ -2 _ _ _ -3 4 _ _ -Answer: 1 2 3 4 -4 3 1 2 +3 _ _ _ +_ _ 4 _ +4 2 _ _ +_ _ _ 4 +Answer: 3 4 1 2 2 1 4 3 -3 4 2 1 -Metadata: {'puzzle': [[1, 0, 0, 0], [0, 0, 1, 0], [2, 0, 0, 0], [3, 4, 0, 0]], 'solution': [[1, 2, 3, 4], [4, 3, 1, 2], [2, 1, 4, 3], [3, 4, 2, 1]], 'num_empty': 11} +4 2 3 1 +1 3 2 4 +Metadata: {'puzzle': [[3, 0, 0, 0], [0, 0, 4, 0], [4, 2, 0, 0], [0, 0, 0, 4]], 'solution': [[3, 4, 1, 2], [2, 1, 4, 3], [4, 2, 3, 1], [1, 3, 2, 4]], 'num_empty': 11} Example 3: Question: Solve this 4x4 Mini Sudoku puzzle: -_ 2 4 3 -_ 3 _ _ -2 _ _ _ -_ 1 2 _ -Answer: 1 2 4 3 -4 3 1 2 -2 4 3 1 +_ _ _ _ +1 3 4 _ 3 1 2 4 -Metadata: {'puzzle': [[0, 2, 4, 3], [0, 3, 0, 0], [2, 0, 0, 0], [0, 1, 2, 0]], 'solution': [[1, 2, 4, 3], [4, 3, 1, 2], [2, 4, 3, 1], [3, 1, 2, 4]], 'num_empty': 9} +4 _ _ _ +Answer: 2 4 1 3 +1 3 4 2 +3 1 2 4 +4 2 3 1 +Metadata: {'puzzle': [[0, 0, 0, 0], [1, 3, 4, 0], [3, 1, 2, 4], [4, 0, 0, 0]], 'solution': [[2, 4, 1, 3], [1, 3, 4, 2], [3, 1, 2, 4], [4, 2, 3, 1]], 'num_empty': 8} + +```` + +### n_queens +Generates N Queens puzzles with configurable difficulty +Default configuration: +```python +n = 8 +min_remove = 1 +max_remove = 7 +size = 500 +seed = 42 ``` -### number_filtering {number-filtering} +Example tasks: +```` +Example 1: +Question: Solve this N Queens puzzle: +_ _ _ _ _ _ Q _ +_ Q _ _ _ _ _ _ +_ _ _ Q _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ _ _ Q +_ _ _ _ Q _ _ _ +_ _ Q _ _ _ _ _ +_ _ _ _ _ Q _ _ + +The board size is 8x8 and your job is to place 1 queen(s) on the board such that no two queens attack each other. + +No two queens attack each other if they are not in the same row, column, or diagonal. + +Place a queen by replacing an underscore (_) with a Q. + +Answer: _ _ _ _ _ _ Q _ +_ Q _ _ _ _ _ _ +_ _ _ Q _ _ _ _ +Q _ _ _ _ _ _ _ +_ _ _ _ _ _ _ Q +_ _ _ _ Q _ _ _ +_ _ Q _ _ _ _ _ +_ _ _ _ _ Q _ _ +Metadata: {'puzzle': [['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', '_', '_', '_', 'Q', '_', '_', '_'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']], 'solutions': [[['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', '_', '_', '_', 'Q', '_', '_', '_'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']]], 'num_removed': 1, 'valid_answers': ['_ _ _ _ _ _ Q _\n_ Q _ _ _ _ _ _\n_ _ _ Q _ _ _ _\nQ _ _ _ _ _ _ _\n_ _ _ _ _ _ _ Q\n_ _ _ _ Q _ _ _\n_ _ Q _ _ _ _ _\n_ _ _ _ _ Q _ _']} + +Example 2: +Question: Solve this N Queens puzzle: +_ Q _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ Q _ _ +_ _ _ _ _ _ _ Q +_ _ _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ _ Q _ +_ _ _ _ Q _ _ _ + +The board size is 8x8 and your job is to place 3 queen(s) on the board such that no two queens attack each other. + +No two queens attack each other if they are not in the same row, column, or diagonal. + +Place a queen by replacing an underscore (_) with a Q. + +Answer: _ Q _ _ _ _ _ _ +_ _ _ Q _ _ _ _ +_ _ _ _ _ Q _ _ +_ _ _ _ _ _ _ Q +_ _ Q _ _ _ _ _ +Q _ _ _ _ _ _ _ +_ _ _ _ _ _ Q _ +_ _ _ _ Q _ _ _ +Metadata: {'puzzle': [['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', '_', '_', '_', 'Q', '_', '_', '_']], 'solutions': [[['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', '_', '_', '_', 'Q', '_', '_', '_']]], 'num_removed': 3, 'valid_answers': ['_ Q _ _ _ _ _ _\n_ _ _ Q _ _ _ _\n_ _ _ _ _ Q _ _\n_ _ _ _ _ _ _ Q\n_ _ Q _ _ _ _ _\nQ _ _ _ _ _ _ _\n_ _ _ _ _ _ Q _\n_ _ _ _ Q _ _ _']} + +Example 3: +Question: Solve this N Queens puzzle: +_ _ _ _ _ _ _ _ +_ Q _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +Q _ _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ _ _ _ +_ _ _ _ _ Q _ _ + +The board size is 8x8 and your job is to place 5 queen(s) on the board such that no two queens attack each other. + +No two queens attack each other if they are not in the same row, column, or diagonal. + +Place a queen by replacing an underscore (_) with a Q. + +Answer: _ _ _ _ Q _ _ _ +_ Q _ _ _ _ _ _ +_ _ _ _ _ _ _ Q +Q _ _ _ _ _ _ _ +_ _ _ Q _ _ _ _ +_ _ _ _ _ _ Q _ +_ _ Q _ _ _ _ _ +_ _ _ _ _ Q _ _ +Metadata: {'puzzle': [['_', '_', '_', '_', '_', '_', '_', '_'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']], 'solutions': [[['_', '_', '_', '_', 'Q', '_', '_', '_'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']], [['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', '_', '_', '_', 'Q', '_', '_', '_'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']], [['_', '_', '_', '_', '_', '_', '_', 'Q'], ['_', 'Q', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', 'Q', '_', '_', '_', '_'], ['Q', '_', '_', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', '_', 'Q', '_'], ['_', '_', '_', '_', 'Q', '_', '_', '_'], ['_', '_', 'Q', '_', '_', '_', '_', '_'], ['_', '_', '_', '_', '_', 'Q', '_', '_']]], 'num_removed': 5, 'valid_answers': ['_ _ _ _ Q _ _ _\n_ Q _ _ _ _ _ _\n_ _ _ _ _ _ _ Q\nQ _ _ _ _ _ _ _\n_ _ _ Q _ _ _ _\n_ _ _ _ _ _ Q _\n_ _ Q _ _ _ _ _\n_ _ _ _ _ Q _ _', '_ _ _ _ _ _ Q _\n_ Q _ _ _ _ _ _\n_ _ _ Q _ _ _ _\nQ _ _ _ _ _ _ _\n_ _ _ _ _ _ _ Q\n_ _ _ _ Q _ _ _\n_ _ Q _ _ _ _ _\n_ _ _ _ _ Q _ _', '_ _ _ _ _ _ _ Q\n_ Q _ _ _ _ _ _\n_ _ _ Q _ _ _ _\nQ _ _ _ _ _ _ _\n_ _ _ _ _ _ Q _\n_ _ _ _ Q _ _ _\n_ _ Q _ _ _ _ _\n_ _ _ _ _ Q _ _']} + +```` + +### number_filtering Generates number filtering tasks Default configuration: @@ -692,30 +1234,30 @@ min_decimals = 0 max_decimals = 4 min_value = -100.0 max_value = 100.0 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Remove all numbers larger than 49.350 in this list: ['-96', '58.6', '39', '4.1432'] -Answer: ['-96', '39', '4.1432'] -Metadata: {'original_numbers': ['-96', '58.6', '39', '4.1432'], 'filter_value': '49.350', 'operation': 'remove_larger', 'result': ['-96', '39', '4.1432']} +Question: Keep all numbers larger than -90 in this list: ['-95.00', '-51.0', '47.2942', '-82.612'] +Answer: ['-51.0', '47.2942', '-82.612'] +Metadata: {'original_numbers': ['-95.00', '-51.0', '47.2942', '-82.612'], 'filter_value': '-90', 'operation': 'keep_larger', 'result': ['-51.0', '47.2942', '-82.612']} Example 2: -Question: Remove all numbers larger than -58.8 in this list: ['42.685', '38.4878', '27.3', '29.6', '-41.16', '87.20', '-66.104', '57.848', '10.3373', '-45.7'] -Answer: ['-66.104'] -Metadata: {'original_numbers': ['42.685', '38.4878', '27.3', '29.6', '-41.16', '87.20', '-66.104', '57.848', '10.3373', '-45.7'], 'filter_value': '-58.8', 'operation': 'remove_larger', 'result': ['-66.104']} +Question: Remove all numbers larger than 18.236 in this list: ['-42.8', '91.88', '34'] +Answer: ['-42.8'] +Metadata: {'original_numbers': ['-42.8', '91.88', '34'], 'filter_value': '18.236', 'operation': 'remove_larger', 'result': ['-42.8']} Example 3: -Question: Keep all numbers smaller than -82.5 in this list: ['-27.517', '11.04', '61', '-95.59', '-89.6322', '84.9458', '-19.8'] -Answer: ['-95.59', '-89.6322'] -Metadata: {'original_numbers': ['-27.517', '11.04', '61', '-95.59', '-89.6322', '84.9458', '-19.8'], 'filter_value': '-82.5', 'operation': 'keep_smaller', 'result': ['-95.59', '-89.6322']} +Question: Keep all numbers larger than 19.8962 in this list: ['4', '-64.7', '-42.1', '-77', '-79.9640', '37.76', '38.702', '18.20', '-28.34'] +Answer: ['37.76', '38.702'] +Metadata: {'original_numbers': ['4', '-64.7', '-42.1', '-77', '-79.9640', '37.76', '38.702', '18.20', '-28.34'], 'filter_value': '19.8962', 'operation': 'keep_larger', 'result': ['37.76', '38.702']} -``` +```` -### number_sequence {number-sequence} +### number_sequence Generates number sequence completion tasks with dynamic pattern generation Default configuration: @@ -725,30 +1267,30 @@ max_terms = 8 min_value = -100 max_value = 100 max_complexity = 3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: 7, 3, 1, 0, 0, 0, ? -Answer: 0 -Metadata: {'rule': 'halve', 'complexity': 2, 'sequence': [7, 3, 1, 0, 0, 0, 0]} +Question: 3, 6, 12, 24, 48, 96, 192, 384, ? +Answer: 768 +Metadata: {'rule': 'double', 'complexity': 3, 'sequence': [3, 6, 12, 24, 48, 96, 192, 384, 768]} Example 2: -Question: -5, -3, -2, -1, ? -Answer: -1 -Metadata: {'rule': 'halve', 'complexity': 3, 'sequence': [-5, -3, -2, -1, -1]} +Question: 8, 14, 20, 26, 32, 38, 44, ? +Answer: 50 +Metadata: {'rule': 'add 6', 'complexity': 1, 'sequence': [8, 14, 20, 26, 32, 38, 44, 50]} Example 3: -Question: 5, 5, 10, 15, 25, 40, 65, ? -Answer: 105 -Metadata: {'rule': 'add previous', 'complexity': 1, 'sequence': [5, 5, 10, 15, 25, 40, 65, 105]} +Question: 8, 4, 2, 1, 0, 0, 0, ? +Answer: 0 +Metadata: {'rule': 'halve', 'complexity': 2, 'sequence': [8, 4, 2, 1, 0, 0, 0, 0]} -``` +```` -### number_sorting {number-sorting} +### number_sorting Generates number sorting tasks Default configuration: @@ -759,30 +1301,30 @@ min_decimals = 0 max_decimals = 2 min_value = -100.0 max_value = 100.0 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Sort these numbers in descending order: 34, 4, -49, -52, -19 -Answer: ['34', '4', '-19', '-49', '-52'] -Metadata: {'original_numbers': ['34', '4', '-49', '-52', '-19'], 'direction': 'descending', 'sorted_numbers': ['34', '4', '-19', '-49', '-52']} +Question: Sort these numbers in ascending order: 48, -51, -72, -80 +Answer: ['-80', '-72', '-51', '48'] +Metadata: {'original_numbers': ['48', '-51', '-72', '-80'], 'direction': 'ascending', 'sorted_numbers': ['-80', '-72', '-51', '48']} Example 2: -Question: Sort these numbers in descending order: -4.44, 91.85, -86.58, -93.98, -92.88, 71.69, 25.88, 57.53, 89.65 -Answer: ['91.85', '89.65', '71.69', '57.53', '25.88', '-4.44', '-86.58', '-92.88', '-93.98'] -Metadata: {'original_numbers': ['-4.44', '91.85', '-86.58', '-93.98', '-92.88', '71.69', '25.88', '57.53', '89.65'], 'direction': 'descending', 'sorted_numbers': ['91.85', '89.65', '71.69', '57.53', '25.88', '-4.44', '-86.58', '-92.88', '-93.98']} +Question: Sort these numbers in ascending order: 39.2, -71.2, -7.5 +Answer: ['-71.2', '-7.5', '39.2'] +Metadata: {'original_numbers': ['39.2', '-71.2', '-7.5'], 'direction': 'ascending', 'sorted_numbers': ['-71.2', '-7.5', '39.2']} Example 3: -Question: Sort these numbers in descending order: -34.19, -85.95, -6.94, -74.52, 5.10, -18.09, -4.41 -Answer: ['5.10', '-4.41', '-6.94', '-18.09', '-34.19', '-74.52', '-85.95'] -Metadata: {'original_numbers': ['-34.19', '-85.95', '-6.94', '-74.52', '5.10', '-18.09', '-4.41'], 'direction': 'descending', 'sorted_numbers': ['5.10', '-4.41', '-6.94', '-18.09', '-34.19', '-74.52', '-85.95']} +Question: Sort these numbers in descending order: 8.39, 72.41, -64.67, -54.97, -94.18, -76.67, -98.24, -68.66, 2.74 +Answer: ['72.41', '8.39', '2.74', '-54.97', '-64.67', '-68.66', '-76.67', '-94.18', '-98.24'] +Metadata: {'original_numbers': ['8.39', '72.41', '-64.67', '-54.97', '-94.18', '-76.67', '-98.24', '-68.66', '2.74'], 'direction': 'descending', 'sorted_numbers': ['72.41', '8.39', '2.74', '-54.97', '-64.67', '-68.66', '-76.67', '-94.18', '-98.24']} -``` +```` -### polynomial_equations {polynomial-equations} +### polynomial_equations Generates random polynomial equations of degree in [min_degree, max_degree]. - The polynomial is formed by summing random terms of the form: coeff * x^exponent. - Then we solve "polynomial_expr = 0" using Sympy. @@ -797,60 +1339,60 @@ max_value = 100 min_degree = 1 max_degree = 3 operators = ('+', '-') -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Find the real value(s) of q in the equation: -166*q**2 - 83*q = 0 -Answer: [-0.5, 0.0] -Metadata: {'polynomial_expr': '-166*q**2 - 83*q', 'variable': 'q', 'degree': 2, 'real_solutions': [-0.5, 0.0]} +Question: Find the real value(s) of u in the equation: -127*u = 0 +Answer: [0.0] +Metadata: {'polynomial_expr': '-127*u', 'variable': 'u', 'degree': 1, 'real_solutions': [0.0]} Example 2: -Question: Determine the real value(s) of i tha satisfies: -41*i = 0 -Answer: [0.0] -Metadata: {'polynomial_expr': '-41*i', 'variable': 'i', 'degree': 1, 'real_solutions': [0.0]} +Question: Determine the real value(s) of b tha satisfies: 86*b**2 - 2*b - 13 = 0 +Answer: [-0.3773425275273891, 0.4005983414808775] +Metadata: {'polynomial_expr': '86*b**2 - 2*b - 13', 'variable': 'b', 'degree': 2, 'real_solutions': [-0.3773425275273891, 0.4005983414808775]} Example 3: -Question: Find the real value(s) of t in the equation: -153*t = 0 -Answer: [0.0] -Metadata: {'polynomial_expr': '-153*t', 'variable': 't', 'degree': 1, 'real_solutions': [0.0]} +Question: Determine the real value(s) of n tha satisfies: 71*n**3 - 2*n - 29 = 0 +Answer: [0.7546129960163634] +Metadata: {'polynomial_expr': '71*n**3 - 2*n - 29', 'variable': 'n', 'degree': 3, 'real_solutions': [0.7546129960163634]} -``` +```` -### prime_factorization {prime-factorization} +### prime_factorization Generates prime factorization tasks Default configuration: ```python min_value = 2 max_value = 1000 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Find the prime factorization of 139. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) -Answer: 139 -Metadata: {'number': 139, 'factors': [139]} +Question: Find the prime factorization of 656. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) +Answer: 2 × 2 × 2 × 2 × 41 +Metadata: {'number': 656, 'factors': [2, 2, 2, 2, 41]} Example 2: -Question: Find the prime factorization of 172. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) -Answer: 2 × 2 × 43 -Metadata: {'number': 172, 'factors': [2, 2, 43]} +Question: Find the prime factorization of 41. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) +Answer: 41 +Metadata: {'number': 41, 'factors': [41]} Example 3: -Question: Find the prime factorization of 562. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) -Answer: 2 × 281 -Metadata: {'number': 562, 'factors': [2, 281]} +Question: Find the prime factorization of 420. Write the factors separated by × (Example: for 12 the answer would be: 2 × 2 × 3) +Answer: 2 × 2 × 3 × 5 × 7 +Metadata: {'number': 420, 'factors': [2, 2, 3, 5, 7]} -``` +```` -### propositional_logic {propositional-logic} +### propositional_logic Generates propositional logic reasoning tasks Default configuration: @@ -860,94 +1402,95 @@ max_vars = 4 min_statements = 2 max_statements = 4 max_complexity = 3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: Given: -1. Q -2. S -3. P +1. R +2. Q What can we conclude? -Answer: (P ∧ S) -Metadata: {'premises': ['Q', 'S', 'P'], 'variables': ['P', 'Q', 'R', 'S'], 'complexity': 3} +Answer: (P ∨ Q) +Metadata: {'premises': ['R', 'Q'], 'variables': ['P', 'Q', 'R', 'S'], 'complexity': 3} Example 2: Question: Given: -1. (P ∨ Q) -2. P +1. ((Q → P) ∨ (Q → P)) +2. ((Q ↔ Q) → (P → P)) +3. P What can we conclude? -Answer: (P ∨ Q) -Metadata: {'premises': ['(P ∨ Q)', 'P'], 'variables': ['P', 'Q', 'R'], 'complexity': 3} +Answer: (P → P) +Metadata: {'premises': ['((Q → P) ∨ (Q → P))', '((Q ↔ Q) → (P → P))', 'P'], 'variables': ['P', 'Q'], 'complexity': 3} Example 3: Question: Given: -1. Q -2. ((Q ↔ P) → (Q → Q)) -3. ((Q → P) → (P ↔ Q)) +1. ((Q ∨ P) ∧ ¬P) +2. P +3. ((P ∧ R) ∧ ¬R) +4. ((Q ↔ R) → ¬Q) What can we conclude? -Answer: (P → Q) -Metadata: {'premises': ['Q', '((Q ↔ P) → (Q → Q))', '((Q → P) → (P ↔ Q))'], 'variables': ['P', 'Q'], 'complexity': 3} +Answer: (Q ∧ Q) +Metadata: {'premises': ['((Q ∨ P) ∧ ¬P)', 'P', '((P ∧ R) ∧ ¬R)', '((Q ↔ R) → ¬Q)'], 'variables': ['P', 'Q', 'R'], 'complexity': 3} -``` +```` -### quantum_lock {quantum-lock} +### quantum_lock Generates QuantumLock tasks Default configuration: ```python difficulty = 10 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: In front of you are some buttons, a light, and a number. The light will toggle between red and green whenever you press a button. Each button performs a mathematical operation to the number, but the operation may depend on the state of the light. -You must press the shortest correct sequence of buttons to reach the target value. +You must press the shortest correct sequence of buttons to reach the target value. Your answer should be a sequence of buttons separated by '→', for example: A → B → C Start: 0 (red) -Target: 36 +Target: 46 Buttons: A: Add 3 (when any) -B: Multiply 3 (when any) -C: Multiply 3 (when red) -Answer: A → B → A → B -Metadata: {'difficulty': 10, 'solution_path': ['A', 'B', 'A', 'B'], 'target_value': 36, 'buttons': [{'name': 'A', 'type': 'add', 'value': 3, 'active_state': 'any'}, {'name': 'B', 'type': 'multiply', 'value': 3, 'active_state': 'any'}, {'name': 'C', 'type': 'multiply', 'value': 3, 'active_state': 'red'}], 'initial_state': 'red', 'initial_value': 0} +B: Add 2 (when any) +C: Multiply 2 (when any) +Answer: A → B → C → C → A → C +Metadata: {'difficulty': 10, 'solution_path': ['A', 'B', 'C', 'C', 'A', 'C'], 'target_value': 46, 'buttons': [{'name': 'A', 'type': 'add', 'value': 3, 'active_state': 'any'}, {'name': 'B', 'type': 'add', 'value': 2, 'active_state': 'any'}, {'name': 'C', 'type': 'multiply', 'value': 2, 'active_state': 'any'}], 'initial_state': 'red', 'initial_value': 0} Example 2: Question: In front of you are some buttons, a light, and a number. The light will toggle between red and green whenever you press a button. Each button performs a mathematical operation to the number, but the operation may depend on the state of the light. -You must press the shortest correct sequence of buttons to reach the target value. +You must press the shortest correct sequence of buttons to reach the target value. Your answer should be a sequence of buttons separated by '→', for example: A → B → C Start: 0 (red) Target: 30 Buttons: -A: Subtract 2 (when red) -B: Add 3 (when any) -C: Subtract 3 (when green) -Answer: B → B → B → B → B → B → B → B → B → B -Metadata: {'difficulty': 10, 'solution_path': ['B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B'], 'target_value': 30, 'buttons': [{'name': 'A', 'type': 'subtract', 'value': 2, 'active_state': 'red'}, {'name': 'B', 'type': 'add', 'value': 3, 'active_state': 'any'}, {'name': 'C', 'type': 'subtract', 'value': 3, 'active_state': 'green'}], 'initial_state': 'red', 'initial_value': 0} +A: Add 2 (when green) +B: Subtract 3 (when red) +C: Multiply 2 (when red) +Answer: C → A → C → A → C → A → C → A +Metadata: {'difficulty': 10, 'solution_path': ['C', 'A', 'C', 'A', 'C', 'A', 'C', 'A'], 'target_value': 30, 'buttons': [{'name': 'A', 'type': 'add', 'value': 2, 'active_state': 'green'}, {'name': 'B', 'type': 'subtract', 'value': 3, 'active_state': 'red'}, {'name': 'C', 'type': 'multiply', 'value': 2, 'active_state': 'red'}], 'initial_state': 'red', 'initial_value': 0} Example 3: Question: In front of you are some buttons, a light, and a number. The light will toggle between red and green whenever you press a button. Each button performs a mathematical operation to the number, but the operation may depend on the state of the light. -You must press the shortest correct sequence of buttons to reach the target value. +You must press the shortest correct sequence of buttons to reach the target value. Your answer should be a sequence of buttons separated by '→', for example: A → B → C Start: 0 (red) -Target: 38 +Target: 45 Buttons: -A: Add 2 (when any) +A: Subtract 2 (when any) B: Add 3 (when any) -C: Subtract 2 (when any) -Answer: A → B → B → B → B → B → B → B → B → B → B → B → B -Metadata: {'difficulty': 10, 'solution_path': ['A', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B'], 'target_value': 38, 'buttons': [{'name': 'A', 'type': 'add', 'value': 2, 'active_state': 'any'}, {'name': 'B', 'type': 'add', 'value': 3, 'active_state': 'any'}, {'name': 'C', 'type': 'subtract', 'value': 2, 'active_state': 'any'}], 'initial_state': 'red', 'initial_value': 0} +C: Add 2 (when any) +Answer: B → B → B → B → B → B → B → B → B → B → B → B → B → B → B +Metadata: {'difficulty': 10, 'solution_path': ['B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B'], 'target_value': 45, 'buttons': [{'name': 'A', 'type': 'subtract', 'value': 2, 'active_state': 'any'}, {'name': 'B', 'type': 'add', 'value': 3, 'active_state': 'any'}, {'name': 'C', 'type': 'add', 'value': 2, 'active_state': 'any'}], 'initial_state': 'red', 'initial_value': 0} -``` +```` -### rubiks_cube {rubiks-cube} +### rubiks_cube Generates RubiksCube tasks Default configuration: @@ -955,99 +1498,99 @@ Default configuration: scramble_steps = 3 cube_size = 3 remove_ansi = True -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: You see a size 3 Rubik's cube. It is arranged this: - - R R R - Y Y Y - R R R - W R W G G G Y O Y B B B - W R W G G G Y O Y B B B - B B B W R W G G G Y O Y - O W O - O W O - O W O - - -Please provide a solution to solve this cube. +Question: You are given a 3x3x3 Rubik's cube. It looks like this: + + G Y G + G Y G + G R G + W W W O G O Y Y Y R B R + R R R W G W O O O Y B Y + R R R W G W O O O Y B Y + B O B + B W B + B W B + + +Please provide a solution to solve this cube using Singmaster notation. Answer: None -Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': "B' F D", 'example_correct_answer': "B F D F' D' F' D B' D' R U' R' L U L' U' R' U R L U' L' U L U' L' U' B' U B U' U' F' U F U R U' R' U' B U' B' U' R' U R U' U' B' U B U L U' L' R U R' U R U U R' U U R U' L' U R' U' L U R U' L' U R' U' L U R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D R' D' R D R' D' R D U"} +Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': "F L' R", 'example_correct_answer': "L F' U' R D B' D' U R U' R' U B U' B' U' R' U R U B U' B' U R' U R U B U' B' U' B' U B U L U' L' U' B' U B U L U' L' U B' U B U L U' L' F R U R' U' F' U' R U R' U R U U R' F U' B' U F' U' B R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D U R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D U"} Example 2: -Question: You see a size 3 Rubik's cube. It is arranged this: - - B O G - B Y G - B Y G - Y Y Y O G W O O O Y B R - R R R Y G W O O O Y B W - R R R Y G R W W W O B W - G W B - G W B - G R B - - -Please provide a solution to solve this cube. +Question: You are given a 3x3x3 Rubik's cube. It looks like this: + + Y Y R + Y Y R + G G R + B B Y R R B W W W G O O + R R W G G G Y O O B B Y + R R W G G G Y O O B B Y + O O O + B W W + B W W + + +Please provide a solution to solve this cube using Singmaster notation. Answer: None -Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': 'B L R', 'example_correct_answer': "R' B' U' L D F' D' U L U' L' U F U' F' U' L' U L U F U' F' U L' U L U F U' F' U' F' U F U R U' R' U' F' U F U R U' R' U F' U F U R U' R' F R U R' U' F' U R U R' U R U U R' L U' R' U L' U' R U L U' R' U L' U' D' R D R' D' R D U R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D R' D' R D R' D' R D U R' D' R D R' D' R D U"} +Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': "L' F U'", 'example_correct_answer': "U' D' B D L' U' F D R' D' U' R U' R' F' U U F U F U' F' U' L' U L U F U' F' U L' U L U F U' F' R U' R' U' F' U F R' U R U B U' B' U' U' B' U B U L U' L' F R U R' U' R U R' U' F' U R U R' U R U U R' U' R U R' U R U U R' U' R U' L' U R' U' L U F U' B' U F' U' B R' D' R D R' D' R D U U R' D' R D R' D' R D U R' D' R D R' D' R D U"} Example 3: -Question: You see a size 3 Rubik's cube. It is arranged this: - - O O O - Y Y G - Y Y G - G R R G G W O O B Y Y Y - Y R R G G W O O W B B B - B B B Y R R G G W O O W - R W W - R W W - R B B - - -Please provide a solution to solve this cube. +Question: You are given a 3x3x3 Rubik's cube. It looks like this: + + Y Y W + Y Y W + Y Y W + G G G O O B O O O G R R + R R R G G B O O O G B B + R R R G G R B B B O B B + W W Y + W W Y + W W Y + + +Please provide a solution to solve this cube using Singmaster notation. Answer: None -Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': 'R B D', 'example_correct_answer': "B' F D F' D' R D R' B' D' L' U' L U R U' R' U' L U U L' F U' F' L' U U L U' B' U B U L U' L' L' U L U F U' F' U' F' U F U R U' R' U' U' R' U R U B U' B' U' U' B' U B U L U' L' U F R U R' U' R U R' U' F' U R U R' U R U U R' L U' R' U L' U' R U L U' R' U L' U' R U R' D' R D R' D' R D U R' D' R D R' D' R D U R' D' R D R' D' R D U"} +Metadata: {'cube_size': 3, 'scramble_steps': 3, 'scramble_moves': "U R' R'", 'example_correct_answer': "R R U'"} -``` +```` -### sentence_reordering {sentence-reordering} +### sentence_reordering Generates sentence reordering tasks from text spans Default configuration: ```python min_words_in_sentence = 3 max_words_in_sentence = 20 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Restore the correct order of words in the following sentence: about We think must it. -Answer: We must think about it. -Metadata: {'word_count': 5} +Question: Restore the correct order of words in the following sentence: wish could get I sleep. "I some +Answer: "I wish I could get some sleep. +Metadata: {'word_count': 7} Example 2: -Question: Restore the correct order of words in the following sentence: 1 through 1. -Answer: 1 through 1. -Metadata: {'word_count': 3} +Question: Restore the correct order of words in the following sentence: the high level name. itself its unable it maintain at was of to Unfortunately, +Answer: Unfortunately, it was unable to maintain itself at the high level of its name. +Metadata: {'word_count': 14} Example 3: -Question: Restore the correct order of words in the following sentence: lease Smith of great of a has falls obtained Niagara. the -Answer: Smith has obtained a lease of of the great falls Niagara. -Metadata: {'word_count': 11} +Question: Restore the correct order of words in the following sentence: developed by For the unutilized. energy falls ages went the +Answer: For ages the the energy developed by falls went unutilized. +Metadata: {'word_count': 10} -``` +```` -### simple_equations {simple-equations} +### simple_equations Generates simple equations with one variable to solve Default configuration: @@ -1057,140 +1600,213 @@ max_terms = 4 min_value = 1 max_value = 100 operators = ('+', '-', '*') -seed = None +seed = 42 size = 500 ``` Example tasks: +```` +Example 1: +Question: Determine the value of u that satisfies: 32*u + 4 = 580 +Answer: 18 +Metadata: {'equation': '32*u + 4 = 580', 'variable': 'u'} + +Example 2: +Question: Solve for b: 82080*b = 1067040 +Answer: 13 +Metadata: {'equation': '82080*b = 1067040', 'variable': 'b'} + +Example 3: +Question: Determine the value of n that satisfies: 29*n - 5 = 430 +Answer: 15 +Metadata: {'equation': '29*n - 5 = 430', 'variable': 'n'} + +```` + +### simple_geometry +A dataset for simple polygon angle-finding tasks. + We randomly choose the number of sides N within [min_sides, max_sides]. + We then generate (N-1) random angles (in degrees), ensuring their sum is + strictly less than the total sum for an (N)-sided convex polygon (which is 180*(N-2)). + The question asks for the missing angle; the answer is computed by subtracting the + sum of known angles from 180*(N-2). + +Default configuration: +```python +min_sides = 3 +max_sides = 6 +min_angle = 10 +max_angle = 170 +seed = 42 +size = 100 ``` + +Example tasks: +```` Example 1: -Question: Find the value of o in the equation: 84*o - 79 = 4625 -Answer: 56 -Metadata: {'equation': '84*o - 79 = 4625', 'variable': 'o'} +Question: Given a convex polygon with 3 sides, its first 2 interior angles are: 16.0°, 80.0°. What is the measure of the remaining interior angle (in degrees)? +Answer: 84 +Metadata: {'n_sides': 3, 'known_angles': [16.0, 80.0], 'sum_of_known_angles': 96.0, 'missing_angle_raw': 84.0, 'missing_angle_rounded': 84, 'total_interior_sum': 180} Example 2: -Question: Find the value of e in the equation: 2068*e = 198528 -Answer: 96 -Metadata: {'equation': '2068*e = 198528', 'variable': 'e'} +Question: A convex polygon has 3 sides. The measures of the first 2 interior angles are: 83.0°, 46.0°. Find the measure of the last interior angle. +Answer: 51 +Metadata: {'n_sides': 3, 'known_angles': [83.0, 46.0], 'sum_of_known_angles': 129.0, 'missing_angle_raw': 51.0, 'missing_angle_rounded': 51, 'total_interior_sum': 180} Example 3: -Question: Determine the value of g that satisfies: 71*g - 80 = 204 -Answer: 4 -Metadata: {'equation': '71*g - 80 = 204', 'variable': 'g'} +Question: Given a convex polygon with 6 sides, its first 5 interior angles are: 143.0°, 148.0°, 39.0°, 55.0°, 107.0°. What is the measure of the remaining interior angle (in degrees)? +Answer: 228 +Metadata: {'n_sides': 6, 'known_angles': [143.0, 148.0, 39.0, 55.0, 107.0], 'sum_of_known_angles': 492.0, 'missing_angle_raw': 228.0, 'missing_angle_rounded': 228, 'total_interior_sum': 720} + +```` + +### simple_integration +Generates simple integration problems with one variable +Default configuration: +```python +min_terms = 2 +max_terms = 5 +min_degree = 1 +max_degree = 10 +min_bounds = 1 +max_bounds = 10 +operators = ('+', '-') +symbols = ('x', 'X') +seed = 42 +size = 500 ``` -### spell_backward {spell-backward} +Example tasks: +```` +Example 1: +Question: Find the indefinite integral: ∫ 70*x**6 + 12*x**2/5 dx +Answer: 10*x**7 + 4*x**3/5 + C +Metadata: {'integrand': '70*x**6 + 12*x**2/5', 'variable': 'x', 'expected_answer_expression': 10*x**7 + 4*x**3/5} + +Example 2: +Question: Find the indefinite integral: ∫ 49*x**6/10 + 48*x**5 - 4*x - 10/9 dx +Answer: 7*x**7/10 + 8*x**6 - 2*x**2 - 10*x/9 + C +Metadata: {'integrand': '49*x**6/10 + 48*x**5 - 4*x - 10/9', 'variable': 'x', 'expected_answer_expression': 7*x**7/10 + 8*x**6 - 2*x**2 - 10*x/9} + +Example 3: +Question: Find the indefinite integral: ∫ -28*X**3 + 8*X dx +Answer: -7*X**4 + 4*X**2 + C +Metadata: {'integrand': '-28*X**3 + 8*X', 'variable': 'X', 'expected_answer_expression': -7*X**4 + 4*X**2} + +```` + +### spell_backward Generates tasks to spell words backward Default configuration: ```python min_word_len = 3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Spell this word backward (example: sun -> nus): made -Answer: edam -Metadata: {'word': 'made', 'word_len': 4} +Question: Spell this word backward (example: sun -> nus): Project +Answer: tcejorP +Metadata: {'word': 'Project', 'word_len': 7} Example 2: -Question: Spell this word backward (example: sun -> nus): then -Answer: neht -Metadata: {'word': 'then', 'word_len': 4} +Question: Spell this word backward (example: sun -> nus): Would +Answer: dluoW +Metadata: {'word': 'Would', 'word_len': 5} Example 3: -Question: Spell this word backward (example: sun -> nus): Europe -Answer: eporuE -Metadata: {'word': 'Europe', 'word_len': 6} +Question: Spell this word backward (example: sun -> nus): One +Answer: enO +Metadata: {'word': 'One', 'word_len': 3} -``` +```` -### sudoku {sudoku} +### sudoku Generates sudoku puzzles with configurable difficulty Default configuration: ```python min_empty = 30 max_empty = 50 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: Solve this Sudoku puzzle: -_ _ 2 _ _ _ 6 7 _ -7 _ _ _ _ 9 _ _ _ -3 _ _ _ 8 7 4 _ _ -_ 8 4 _ 7 _ 9 _ _ -_ _ _ _ _ _ 3 _ _ -9 _ 3 1 _ _ _ 8 7 -_ 1 8 4 9 _ _ 5 3 -_ _ _ 8 5 1 2 9 4 -4 5 9 _ _ 2 _ _ 6 -Answer: 8 4 2 5 1 3 6 7 9 -7 6 5 2 4 9 1 3 8 -3 9 1 6 8 7 4 2 5 -1 8 4 3 7 5 9 6 2 -5 7 6 9 2 8 3 4 1 -9 2 3 1 6 4 5 8 7 -2 1 8 4 9 6 7 5 3 -6 3 7 8 5 1 2 9 4 -4 5 9 7 3 2 8 1 6 -Metadata: {'puzzle': [[0, 0, 2, 0, 0, 0, 6, 7, 0], [7, 0, 0, 0, 0, 9, 0, 0, 0], [3, 0, 0, 0, 8, 7, 4, 0, 0], [0, 8, 4, 0, 7, 0, 9, 0, 0], [0, 0, 0, 0, 0, 0, 3, 0, 0], [9, 0, 3, 1, 0, 0, 0, 8, 7], [0, 1, 8, 4, 9, 0, 0, 5, 3], [0, 0, 0, 8, 5, 1, 2, 9, 4], [4, 5, 9, 0, 0, 2, 0, 0, 6]], 'solution': [[8, 4, 2, 5, 1, 3, 6, 7, 9], [7, 6, 5, 2, 4, 9, 1, 3, 8], [3, 9, 1, 6, 8, 7, 4, 2, 5], [1, 8, 4, 3, 7, 5, 9, 6, 2], [5, 7, 6, 9, 2, 8, 3, 4, 1], [9, 2, 3, 1, 6, 4, 5, 8, 7], [2, 1, 8, 4, 9, 6, 7, 5, 3], [6, 3, 7, 8, 5, 1, 2, 9, 4], [4, 5, 9, 7, 3, 2, 8, 1, 6]], 'num_empty': 45} +4 _ _ _ 5 2 _ 3 _ +_ _ 3 4 6 _ _ _ _ +6 1 2 _ _ 8 4 _ _ +1 _ _ _ _ _ 7 9 5 +3 _ _ 7 1 _ _ 2 6 +7 _ _ 5 _ _ _ _ 3 +2 _ _ _ 7 5 _ _ _ +_ 3 _ _ 4 1 _ _ _ +_ _ _ 2 8 _ _ _ 4 +Answer: 4 7 8 1 5 2 6 3 9 +5 9 3 4 6 7 2 8 1 +6 1 2 3 9 8 4 5 7 +1 2 4 8 3 6 7 9 5 +3 5 9 7 1 4 8 2 6 +7 8 6 5 2 9 1 4 3 +2 4 1 9 7 5 3 6 8 +8 3 5 6 4 1 9 7 2 +9 6 7 2 8 3 5 1 4 +Metadata: {'puzzle': [[4, 0, 0, 0, 5, 2, 0, 3, 0], [0, 0, 3, 4, 6, 0, 0, 0, 0], [6, 1, 2, 0, 0, 8, 4, 0, 0], [1, 0, 0, 0, 0, 0, 7, 9, 5], [3, 0, 0, 7, 1, 0, 0, 2, 6], [7, 0, 0, 5, 0, 0, 0, 0, 3], [2, 0, 0, 0, 7, 5, 0, 0, 0], [0, 3, 0, 0, 4, 1, 0, 0, 0], [0, 0, 0, 2, 8, 0, 0, 0, 4]], 'solution': [[4, 7, 8, 1, 5, 2, 6, 3, 9], [5, 9, 3, 4, 6, 7, 2, 8, 1], [6, 1, 2, 3, 9, 8, 4, 5, 7], [1, 2, 4, 8, 3, 6, 7, 9, 5], [3, 5, 9, 7, 1, 4, 8, 2, 6], [7, 8, 6, 5, 2, 9, 1, 4, 3], [2, 4, 1, 9, 7, 5, 3, 6, 8], [8, 3, 5, 6, 4, 1, 9, 7, 2], [9, 6, 7, 2, 8, 3, 5, 1, 4]], 'num_empty': 48} Example 2: Question: Solve this Sudoku puzzle: -3 5 _ _ _ _ _ _ _ -_ 1 _ 3 _ 8 5 4 6 -7 _ 8 9 _ _ _ 3 2 -2 3 7 _ 4 _ _ 8 _ -_ _ 1 8 _ 2 3 _ 4 -_ _ 4 7 9 3 6 _ _ -8 6 _ _ _ _ 2 _ _ -_ 2 _ _ 8 7 _ _ _ -_ _ _ 6 2 _ 8 5 _ -Answer: 3 5 6 2 1 4 7 9 8 -9 1 2 3 7 8 5 4 6 -7 4 8 9 6 5 1 3 2 -2 3 7 1 4 6 9 8 5 -6 9 1 8 5 2 3 7 4 -5 8 4 7 9 3 6 2 1 -8 6 5 4 3 9 2 1 7 -1 2 3 5 8 7 4 6 9 -4 7 9 6 2 1 8 5 3 -Metadata: {'puzzle': [[3, 5, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 3, 0, 8, 5, 4, 6], [7, 0, 8, 9, 0, 0, 0, 3, 2], [2, 3, 7, 0, 4, 0, 0, 8, 0], [0, 0, 1, 8, 0, 2, 3, 0, 4], [0, 0, 4, 7, 9, 3, 6, 0, 0], [8, 6, 0, 0, 0, 0, 2, 0, 0], [0, 2, 0, 0, 8, 7, 0, 0, 0], [0, 0, 0, 6, 2, 0, 8, 5, 0]], 'solution': [[3, 5, 6, 2, 1, 4, 7, 9, 8], [9, 1, 2, 3, 7, 8, 5, 4, 6], [7, 4, 8, 9, 6, 5, 1, 3, 2], [2, 3, 7, 1, 4, 6, 9, 8, 5], [6, 9, 1, 8, 5, 2, 3, 7, 4], [5, 8, 4, 7, 9, 3, 6, 2, 1], [8, 6, 5, 4, 3, 9, 2, 1, 7], [1, 2, 3, 5, 8, 7, 4, 6, 9], [4, 7, 9, 6, 2, 1, 8, 5, 3]], 'num_empty': 43} +_ _ _ 1 3 2 6 4 5 +_ 4 _ 7 _ _ _ 9 1 +_ _ 1 8 _ 9 _ _ _ +_ 8 9 _ _ _ 7 5 4 +_ 3 _ 4 _ 1 9 8 _ +4 6 _ 5 9 _ 1 2 3 +5 _ 4 9 1 7 3 _ _ +9 7 6 _ 8 4 5 1 _ +8 _ 3 _ _ _ 4 7 _ +Answer: 7 9 8 1 3 2 6 4 5 +3 4 2 7 5 6 8 9 1 +6 5 1 8 4 9 2 3 7 +1 8 9 6 2 3 7 5 4 +2 3 5 4 7 1 9 8 6 +4 6 7 5 9 8 1 2 3 +5 2 4 9 1 7 3 6 8 +9 7 6 3 8 4 5 1 2 +8 1 3 2 6 5 4 7 9 +Metadata: {'puzzle': [[0, 0, 0, 1, 3, 2, 6, 4, 5], [0, 4, 0, 7, 0, 0, 0, 9, 1], [0, 0, 1, 8, 0, 9, 0, 0, 0], [0, 8, 9, 0, 0, 0, 7, 5, 4], [0, 3, 0, 4, 0, 1, 9, 8, 0], [4, 6, 0, 5, 9, 0, 1, 2, 3], [5, 0, 4, 9, 1, 7, 3, 0, 0], [9, 7, 6, 0, 8, 4, 5, 1, 0], [8, 0, 3, 0, 0, 0, 4, 7, 0]], 'solution': [[7, 9, 8, 1, 3, 2, 6, 4, 5], [3, 4, 2, 7, 5, 6, 8, 9, 1], [6, 5, 1, 8, 4, 9, 2, 3, 7], [1, 8, 9, 6, 2, 3, 7, 5, 4], [2, 3, 5, 4, 7, 1, 9, 8, 6], [4, 6, 7, 5, 9, 8, 1, 2, 3], [5, 2, 4, 9, 1, 7, 3, 6, 8], [9, 7, 6, 3, 8, 4, 5, 1, 2], [8, 1, 3, 2, 6, 5, 4, 7, 9]], 'num_empty': 34} Example 3: Question: Solve this Sudoku puzzle: -2 _ 1 4 _ 5 6 _ _ -_ 8 _ 6 _ 1 5 2 9 -_ _ _ _ _ 2 _ 3 _ -1 _ 4 2 _ _ _ _ 5 -_ _ _ _ 4 _ _ 6 _ -_ _ 9 _ _ _ 2 4 _ -8 _ _ 5 1 6 3 _ 7 -9 _ _ 7 _ 3 _ 1 2 -3 _ _ 9 _ 4 _ _ 6 -Answer: 2 9 1 4 3 5 6 7 8 -4 8 3 6 7 1 5 2 9 -7 5 6 8 9 2 1 3 4 -1 3 4 2 6 7 9 8 5 -5 2 8 1 4 9 7 6 3 -6 7 9 3 5 8 2 4 1 -8 4 2 5 1 6 3 9 7 -9 6 5 7 8 3 4 1 2 -3 1 7 9 2 4 8 5 6 -Metadata: {'puzzle': [[2, 0, 1, 4, 0, 5, 6, 0, 0], [0, 8, 0, 6, 0, 1, 5, 2, 9], [0, 0, 0, 0, 0, 2, 0, 3, 0], [1, 0, 4, 2, 0, 0, 0, 0, 5], [0, 0, 0, 0, 4, 0, 0, 6, 0], [0, 0, 9, 0, 0, 0, 2, 4, 0], [8, 0, 0, 5, 1, 6, 3, 0, 7], [9, 0, 0, 7, 0, 3, 0, 1, 2], [3, 0, 0, 9, 0, 4, 0, 0, 6]], 'solution': [[2, 9, 1, 4, 3, 5, 6, 7, 8], [4, 8, 3, 6, 7, 1, 5, 2, 9], [7, 5, 6, 8, 9, 2, 1, 3, 4], [1, 3, 4, 2, 6, 7, 9, 8, 5], [5, 2, 8, 1, 4, 9, 7, 6, 3], [6, 7, 9, 3, 5, 8, 2, 4, 1], [8, 4, 2, 5, 1, 6, 3, 9, 7], [9, 6, 5, 7, 8, 3, 4, 1, 2], [3, 1, 7, 9, 2, 4, 8, 5, 6]], 'num_empty': 44} - -``` - -### syllogism {syllogism} +_ _ 1 2 3 _ _ _ 9 +3 _ _ 1 8 5 6 7 2 +_ _ _ 4 9 6 1 _ _ +1 _ 5 7 _ _ 9 2 _ +_ 4 _ _ 5 9 7 1 6 +9 _ 6 _ 1 _ 4 5 3 +_ _ 3 9 7 _ 2 8 4 +_ _ 2 6 4 _ _ 9 1 +_ 1 _ 5 2 8 3 _ _ +Answer: 5 6 1 2 3 7 8 4 9 +3 9 4 1 8 5 6 7 2 +8 2 7 4 9 6 1 3 5 +1 3 5 7 6 4 9 2 8 +2 4 8 3 5 9 7 1 6 +9 7 6 8 1 2 4 5 3 +6 5 3 9 7 1 2 8 4 +7 8 2 6 4 3 5 9 1 +4 1 9 5 2 8 3 6 7 +Metadata: {'puzzle': [[0, 0, 1, 2, 3, 0, 0, 0, 9], [3, 0, 0, 1, 8, 5, 6, 7, 2], [0, 0, 0, 4, 9, 6, 1, 0, 0], [1, 0, 5, 7, 0, 0, 9, 2, 0], [0, 4, 0, 0, 5, 9, 7, 1, 6], [9, 0, 6, 0, 1, 0, 4, 5, 3], [0, 0, 3, 9, 7, 0, 2, 8, 4], [0, 0, 2, 6, 4, 0, 0, 9, 1], [0, 1, 0, 5, 2, 8, 3, 0, 0]], 'solution': [[5, 6, 1, 2, 3, 7, 8, 4, 9], [3, 9, 4, 1, 8, 5, 6, 7, 2], [8, 2, 7, 4, 9, 6, 1, 3, 5], [1, 3, 5, 7, 6, 4, 9, 2, 8], [2, 4, 8, 3, 5, 9, 7, 1, 6], [9, 7, 6, 8, 1, 2, 4, 5, 3], [6, 5, 3, 9, 7, 1, 2, 8, 4], [7, 8, 2, 6, 4, 3, 5, 9, 1], [4, 1, 9, 5, 2, 8, 3, 6, 7]], 'num_empty': 33} + +```` + +### syllogism Generates syllogism reasoning tasks Default configuration: @@ -1202,110 +1818,209 @@ allow_some = True allow_some_not = True include_invalid = True invalid_ratio = 0.3 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: Question: Consider these statements: -1. Some programmers are cats -2. Some ... are not cats are engineers +1. No students are humans +2. No humans are chefs Does it logically follow that: -No programmers are engineers? +No students are chefs? (Answer Yes or No) Answer: Yes -Metadata: {'premise1': 'Some programmers are cats', 'premise2': 'Some ... are not cats are engineers', 'conclusion': 'No programmers are engineers', 'is_valid': True} +Metadata: {'premise1': 'No students are humans', 'premise2': 'No humans are chefs', 'conclusion': 'No students are chefs', 'is_valid': True} Example 2: Question: Consider these statements: -1. All parents are cats -2. Some cats are lawyers +1. Some children are not animals +2. Some animals are doctors Does it logically follow that: -Some ... are not parents are lawyers? +All children are doctors? (Answer Yes or No) Answer: Yes -Metadata: {'premise1': 'All parents are cats', 'premise2': 'Some cats are lawyers', 'conclusion': 'Some ... are not parents are lawyers', 'is_valid': True} +Metadata: {'premise1': 'Some children are not animals', 'premise2': 'Some animals are doctors', 'conclusion': 'All children are doctors', 'is_valid': True} Example 3: Question: Consider these statements: -1. No whales are birds -2. Some birds are teachers +1. All butterflies are tigers +2. No tigers are whales Does it logically follow that: -All whales are teachers? +Some butterflies are not whales? (Answer Yes or No) -Answer: Yes -Metadata: {'premise1': 'No whales are birds', 'premise2': 'Some birds are teachers', 'conclusion': 'All whales are teachers', 'is_valid': True} +Answer: No +Metadata: {'premise1': 'All butterflies are tigers', 'premise2': 'No tigers are whales', 'conclusion': 'Some butterflies are not whales', 'is_valid': False} + +```` + +### time_intervals +Generates time interval calculation tasks with various formats and complexities + +Default configuration: +```python +min_time = 00:00:00 +max_time = 23:59:59.999999 +max_time_difference_seconds = 86400 +min_date = 1900-01-01 +max_date = 3000-01-01 +max_date_difference_days = 100 +task_types = ['time', 'time_seconds', 'time_ms', 'date', 'datetime', 'datetime_tz'] +seed = 42 +size = 500 +``` + +Example tasks: +```` +Example 1: +Question: A system backup started at 2964-06-17 08:15:14 and completed at 2964-07-04 11:59:09. What was the total backup duration? Answer in D days, HH:MM. +Answer: 17 days, 03:43 +Metadata: {'task_type': 'datetime_tz', 'start_time': datetime.datetime(2964, 6, 17, 8, 15, 14), 'end_time': datetime.datetime(2964, 7, 4, 11, 59, 9), 'format': '%Y-%m-%d %H:%M:%S', 'expected_format': 'D days, HH:MM'} + +Example 2: +Question: A video call started at 09:44 and ended at 12:22. How long was the call? Answer in HH:MM. +Answer: 02:38 +Metadata: {'task_type': 'time', 'start_time': datetime.datetime(2025, 2, 2, 9, 44), 'end_time': datetime.datetime(2025, 2, 2, 12, 22), 'format': '%H:%M', 'expected_format': 'HH:MM'} + +Example 3: +Question: Calculate the time difference between Sat Dec 22 2677 and Thu Mar 21 2678. Express the result in D days. +Answer: 89 days +Metadata: {'task_type': 'date', 'start_time': datetime.datetime(2677, 12, 22, 0, 0), 'end_time': datetime.datetime(2678, 3, 21, 0, 0), 'format': '%a %b %d %Y', 'expected_format': 'D days'} + +```` + +### tower_of_hanoi +Generates Tower of Hanoi problems with solutions. + Supports variable number of pegs using the optimized Frame-Stewart algorithm with Peg State Tracking. +Default configuration: +```python +min_disks = 3 +max_disks = 7 +min_pegs = 3 +max_pegs = 4 +size = 50 +seed = 42 +visualize = False ``` -### word_ladder {word-ladder} +Example tasks: +```` +Example 1: +Question: Solve the Tower of Hanoi problem with 3 disks and 3 pegs. +Move all disks from Peg 3 to Peg 2 following the rules: +- Only one disk can be moved at a time. +- A larger disk cannot be placed on top of a smaller disk. +- All disks must be on a peg at all times. +Example: +Move disk 1 from Peg 1 to Peg 3 +Move disk 2 from Peg 1 to Peg 2 +Move disk 1 from Peg 3 to Peg 2 + +Provide the sequence of moves. +Answer: ['Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 3 from Peg 3 to Peg 2', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2'] +Metadata: {'num_disks': 3, 'num_pegs': 3, 'start_peg': 3, 'target_peg': 2, 'auxiliary_pegs': [1], 'solution_length': 7} + +Example 2: +Question: Solve the Tower of Hanoi problem with 3 disks and 4 pegs. +Move all disks from Peg 2 to Peg 4 following the rules: +- Only one disk can be moved at a time. +- A larger disk cannot be placed on top of a smaller disk. +- All disks must be on a peg at all times. +Example: +Move disk 1 from Peg 1 to Peg 3 +Move disk 2 from Peg 1 to Peg 2 +Move disk 1 from Peg 3 to Peg 2 + +Provide the sequence of moves. +Answer: ['Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 3 from Peg 2 to Peg 4', 'Move disk 2 from Peg 3 to Peg 4', 'Move disk 1 from Peg 1 to Peg 4'] +Metadata: {'num_disks': 3, 'num_pegs': 4, 'start_peg': 2, 'target_peg': 4, 'auxiliary_pegs': [1, 3], 'solution_length': 5} + +Example 3: +Question: Solve the Tower of Hanoi problem with 6 disks and 3 pegs. +Move all disks from Peg 1 to Peg 2 following the rules: +- Only one disk can be moved at a time. +- A larger disk cannot be placed on top of a smaller disk. +- All disks must be on a peg at all times. +Example: +Move disk 1 from Peg 1 to Peg 3 +Move disk 2 from Peg 1 to Peg 2 +Move disk 1 from Peg 3 to Peg 2 + +Provide the sequence of moves. +Answer: ['Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 3 from Peg 1 to Peg 3', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 4 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 3 from Peg 3 to Peg 2', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 5 from Peg 1 to Peg 3', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 3 from Peg 2 to Peg 1', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 4 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 3 from Peg 1 to Peg 3', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 6 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 3 from Peg 3 to Peg 2', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 4 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 3 from Peg 2 to Peg 1', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 5 from Peg 3 to Peg 2', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 3 from Peg 1 to Peg 3', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 2 from Peg 2 to Peg 3', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 4 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2', 'Move disk 2 from Peg 3 to Peg 1', 'Move disk 1 from Peg 2 to Peg 1', 'Move disk 3 from Peg 3 to Peg 2', 'Move disk 1 from Peg 1 to Peg 3', 'Move disk 2 from Peg 1 to Peg 2', 'Move disk 1 from Peg 3 to Peg 2'] +Metadata: {'num_disks': 6, 'num_pegs': 3, 'start_peg': 1, 'target_peg': 2, 'auxiliary_pegs': [3], 'solution_length': 63} + +```` + +### word_ladder Generates word ladder transformation tasks Default configuration: ```python -min_word_length = 4 -max_word_length = 4 +min_word_length = 3 +max_word_length = 5 min_chain_length = -1 max_chain_length = -1 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Transform the word ladder 'COLD' to 'WARM' by changing one letter at a time. -Answer: COLD,CORD,CARD,WARD,WARM -Metadata: {'start_word': 'COLD', 'end_word': 'WARM', 'word_length': 4, 'chain_length': 5} +Question: Transform the word 'CEILS' into 'ANIGH' by changing one letter at a time. Each step must create a valid English word (including plurals) and keep the same word length. Show the sequence of words needed. +Answer: CEILS,TEILS,TEINS,THINS,THIGS,THIGH,AHIGH,ANIGH +Metadata: {'start_word': 'CEILS', 'end_word': 'ANIGH', 'word_length': 5, 'chain_length': 8} Example 2: -Question: Transform the word ladder 'DARK' to 'LIGHT' by changing one letter at a time. -Answer: DARK,DARE,DATE,LATE,LITE,LIGHT -Metadata: {'start_word': 'DARK', 'end_word': 'LIGHT', 'word_length': 4, 'chain_length': 6} +Question: Transform the word 'KAW' into 'EFS' by changing one letter at a time. Each step must create a valid English word (including plurals) and keep the same word length. Show the sequence of words needed. +Answer: KAW,KAS,EAS,EFS +Metadata: {'start_word': 'KAW', 'end_word': 'EFS', 'word_length': 3, 'chain_length': 4} Example 3: -Question: Transform the word ladder 'HEAD' to 'TAIL' by changing one letter at a time. -Answer: HEAD,HEAL,TEAL,TAIL -Metadata: {'start_word': 'HEAD', 'end_word': 'TAIL', 'word_length': 4, 'chain_length': 4} +Question: Transform the word 'SAUT' into 'SKER' by changing one letter at a time. Each step must create a valid English word (including plurals) and keep the same word length. Show the sequence of words needed. +Answer: SAUT,SHUT,SHET,SKET,SKER +Metadata: {'start_word': 'SAUT', 'end_word': 'SKER', 'word_length': 4, 'chain_length': 5} -``` +```` -### word_sequence_reversal {word-sequence-reversal} +### word_sequence_reversal Generates word sequence reversal tasks from text spans Default configuration: ```python min_words = 3 max_words = 8 -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Reverse this list of words: upon, bold, what, of, have -Answer: have, of, what, bold, upon -Metadata: {'num_words': 5, 'words': ['upon', 'bold', 'what', 'of', 'have']} +Question: Reverse this list of words: bed, if, problem, but, Well, an, transmission, nutritive +Answer: nutritive, transmission, an, Well, but, problem, if, bed +Metadata: {'num_words': 8, 'words': ['bed', 'if', 'problem', 'but', 'Well', 'an', 'transmission', 'nutritive']} Example 2: -Question: Reverse this list of words: years, WILL, Gutenberg, Nevertheless -Answer: Nevertheless, Gutenberg, WILL, years -Metadata: {'num_words': 4, 'words': ['years', 'WILL', 'Gutenberg', 'Nevertheless']} +Question: Reverse this list of words: it, pleasure, Gutenberg +Answer: Gutenberg, pleasure, it +Metadata: {'num_words': 3, 'words': ['it', 'pleasure', 'Gutenberg']} Example 3: -Question: Reverse this list of words: or, of, With, no -Answer: no, With, of, or -Metadata: {'num_words': 4, 'words': ['or', 'of', 'With', 'no']} +Question: Reverse this list of words: readable, to, he, that, to, possession +Answer: possession, to, that, he, to, readable +Metadata: {'num_words': 6, 'words': ['readable', 'to', 'he', 'that', 'to', 'possession']} -``` +```` -### word_sorting {word-sorting} +### word_sorting Generates word sorting tasks Default configuration: @@ -1315,28 +2030,30 @@ max_words = 10 min_word_length = 3 max_word_length = 12 transformation = original -seed = None +seed = 42 size = 500 ``` Example tasks: -``` +```` Example 1: -Question: Sort these words in descending order (using ASCII/Unicode ordering) and return them as a comma-separated list: -believe, content, How, dedicated, seasons -Answer: seasons, dedicated, content, believe, How -Metadata: {'original_words': ['believe', 'content', 'How', 'dedicated', 'seasons'], 'transformed_words': ['believe', 'content', 'How', 'dedicated', 'seasons'], 'direction': 'descending', 'transformation': , 'sorted_words': ['seasons', 'dedicated', 'content', 'believe', 'How']} +Question: Sort these words in ascending order (using ASCII/Unicode ordering) and return them as a comma-separated list: +DIRECT, given, exclaims, dreaming +Answer: DIRECT, dreaming, exclaims, given +Metadata: {'original_words': ['DIRECT', 'given', 'exclaims', 'dreaming'], 'transformed_words': ['DIRECT', 'given', 'exclaims', 'dreaming'], 'direction': 'ascending', 'transformation': , 'sorted_words': ['DIRECT', 'dreaming', 'exclaims', 'given']} Example 2: -Question: Sort these words in ascending order (using ASCII/Unicode ordering) and return them as a comma-separated list: -owing, acute, included -Answer: acute, included, owing -Metadata: {'original_words': ['owing', 'acute', 'included'], 'transformed_words': ['owing', 'acute', 'included'], 'direction': 'ascending', 'transformation': , 'sorted_words': ['acute', 'included', 'owing']} +Question: Sort these words in descending order (using ASCII/Unicode ordering) and return them as a comma-separated list: +heat, begun, sometimes +Answer: sometimes, heat, begun +Metadata: {'original_words': ['heat', 'begun', 'sometimes'], 'transformed_words': ['heat', 'begun', 'sometimes'], 'direction': 'descending', 'transformation': , 'sorted_words': ['sometimes', 'heat', 'begun']} Example 3: Question: Sort these words in ascending order (using ASCII/Unicode ordering) and return them as a comma-separated list: -WARRANTY, tell, territory, Reckon, downloading -Answer: Reckon, WARRANTY, downloading, tell, territory -Metadata: {'original_words': ['WARRANTY', 'tell', 'territory', 'Reckon', 'downloading'], 'transformed_words': ['WARRANTY', 'tell', 'territory', 'Reckon', 'downloading'], 'direction': 'ascending', 'transformation': , 'sorted_words': ['Reckon', 'WARRANTY', 'downloading', 'tell', 'territory']} +violates, yes, already, completing, pages, duty, his, EXPRESS, duly +Answer: EXPRESS, already, completing, duly, duty, his, pages, violates, yes +Metadata: {'original_words': ['violates', 'yes', 'already', 'completing', 'pages', 'duty', 'his', 'EXPRESS', 'duly'], 'transformed_words': ['violates', 'yes', 'already', 'completing', 'pages', 'duty', 'his', 'EXPRESS', 'duly'], 'direction': 'ascending', 'transformation': , 'sorted_words': ['EXPRESS', 'already', 'completing', 'duly', 'duty', 'his', 'pages', 'violates', 'yes']} + +```` + -``` diff --git a/README.md b/README.md index 0aaba34a..a1aa3791 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,37 @@ We are building a python library of procedural dataset generators and algorithmi The goal is to generate virtually infinite data with adjustable complexity. -Algorithmic verification allows to train on tasks like Rubik‘s cube or [Countdown](https://en.wikipedia.org/wiki/Countdown_(game_show)#Numbers_Round) which have many correct solutions. +Algorithmic verification allows to train on tasks like Rubik‘s cube or [Countdown]() which have many correct solutions. ## Set up for development + 1. Clone the project + ``` git clone https://github.com/open-thought/reasoning-gym.git ``` + 2. Create a virtual environment (here we use conda) + ``` conda create --name reasoning_gym python=3.11 -y conda activate reasoning_gym ``` + 3. Link project and install dependencies + ``` pip install -e . ``` + 4. Install development dependencies + ``` pip install -r requirements-dev.txt ``` ->NOTE: To consume the APIs in reasoning_gym, just install from pip using the following +> NOTE: To consume the APIs in reasoning_gym, just install from pip using the following + ``` pip install reasoning-gym ``` @@ -67,12 +76,14 @@ See the [Dataset Gallery](GALLERY.md) for a complete list of available datasets ### Arithmetic Tasks - `BasicArithmeticDataset`: Generate arithmetic expressions with configurable complexity and operators (+, -, \*, /) +- `CalendarArithmeticDatset`: Generate arithmetic problems around calendar navigation logic - `ChainSum`: Generate addition/subtraction chains with configurable length and digit counts - `FractionSimplificationDataset`: Generate fraction simplification tasks with configurable complexity - `GCDDataset`: Generate Greatest Common Divisor problems with configurable number of integers - `LCMDataset`: Generate Least Common Multiple problems with configurable number of integers - `LegCountingDataset`: Generate animal leg counting word problems with various animals - `PrimeFactorizationDataset`: Generate prime factorization tasks with configurable number ranges +- `TimeIntervalsDataset`: Generate time interval calculation tasks with various formats (time, date, datetime) and complexities ### Algorithmic Tasks @@ -88,18 +99,26 @@ See the [Dataset Gallery](GALLERY.md) for a complete list of available datasets - `WordSequenceReversalDataset`: Reverse word order in text spans - `WordLadderDataset`: Generate word ladder puzzles where one word is transformed into another by changing one letter at a time +### Code Tasks + +- `BFDataset`: Generates BF programs of various difficult, from simple string printing to loops and conditional logic + ### Cognition Tasks - `NumberSequenceDataset`: Generate number sequences with discoverable patterns - `ColorCubeRotationDataset`: Generate 3D spatial reasoning tasks with colored cube rotations and orientation tracking +- `RubiksCubeDataset`: Generate Rubik's Cube configurations and check correct solutions +- `FigletFontDataset`: Generate random words in different "Figlet" fonts for reasoning about the structure of letters ### Logic Tasks - `PropositionalLogicDataset`: Generate propositional logic reasoning problems - +- `SyllogismDataset`: Generates a [syllogism](https://en.wikipedia.org/wiki/Syllogism) reasoning dataset +- `AliceInWonderlandDataset`: Generates [AIW](https://openreview.net/forum?id=Mkl7dzjYiW) (Alice In Wonderland) problems with a few variations ### Graph Tasks - `FamilyRelationshipsDataset`: Generate family relationship reasoning tasks with family trees +- `QuantumLockDataset`: Generates puzzles which involve stateful arithmetic and a correct sequence of operations ### Game Tasks @@ -107,291 +126,7 @@ See the [Dataset Gallery](GALLERY.md) for a complete list of available datasets - `MiniSudokuDataset`: Generate 4x4 Mini Sudoku puzzles with configurable difficulty - `MazeDataset`: Generate a maze with a start and a goal - `CountdownDataset`: Generate number game tasks where numbers and operators must be combined to reach a target value - -## Available Generators - -
- -

PolynomialEquations

-Generate polynomial equations with configurable complexity: -
- -```python -from reasoning_gym.algebra import PolynomialEquationsConfig, PolynomialEquationsConfig - -config = PolynomialEquationsConfig( - min_terms=3, - max_terms=4, - min_degree=4, - max_degree=4, - min_value=1, - max_value=5, - size=3, - seed=123, -) - -dataset = PolynomialEquationsDataset(config) -for item in dataset: - print(item) -``` - -Example output: - -``` -{'question': 'Find the real value(s) of b in the equation: b**4 - b**3 - 5*b**2 = 0', 'answer': '[-1.79128784747792, 0.0, 2.79128784747792]', 'metadata': {'polynomial_expr': 'b**4 - b**3 - 5*b**2', 'variable': 'b', 'degree': 4, 'real_solutions': [-1.79128784747792, 0.0, 2.79128784747792]}} -{'question': 'Solve the polynomial equation for real i:\n3*i**4 + 4*i**3 - 1 = 0', 'answer': '[]', 'metadata': {'polynomial_expr': '3*i**4 + 4*i**3 - 1', 'variable': 'i', 'degree': 4, 'real_solutions': []}} -{'question': 'Solve the polynomial equation for real h:\n7*h**4 - 2*h**2 + h = 0', 'answer': '[-0.6998793469266564, 0.0]', 'metadata': {'polynomial_expr': '7*h**4 - 2*h**2 + h', 'variable': 'h', 'degree': 4, 'real_solutions': [-0.6998793469266564, 0.0]}} -``` -
- -
- -

Basic Arithmetic

-Generate arithmetic problems with configurable complexity: -
- -```python -from reasoning_gym.arithmetic import BasicArithmeticDataset, BasicArithmeticDatasetConfig - -config = BasicArithmeticDatasetConfig( - min_terms=2, # Minimum number of terms in expression - max_terms=4, # Maximum number of terms - min_digits=1, # Minimum digits per number - max_digits=2, # Maximum digits per number - allow_parentheses=True, # Include nested expressions - size=5, # Number of problems to generate - seed=42 # For reproducibility -) - -dataset = BasicArithmeticDataset(config) -for item in dataset: - print(item) -``` - -Example output: - -``` -{'question': '-1 + -5 * 8 + -8 =', 'answer': '-49', 'metadata': {'num_terms': 4, 'num_digits': 1, 'expression': '-1 + -5 * 8 + -8'}} -{'question': '19 - 17 =', 'answer': '2', 'metadata': {'num_terms': 2, 'num_digits': 2, 'expression': '19 - 17'}} -{'question': '3 + -6 * -9 =', 'answer': '57', 'metadata': {'num_terms': 3, 'num_digits': 1, 'expression': '3 + -6 * -9'}} -{'question': '-22 - -94 + -97 =', 'answer': '-25', 'metadata': {'num_terms': 3, 'num_digits': 2, 'expression': '-22 - -94 + -97'}} -{'question': '51 * 63 =', 'answer': '3213', 'metadata': {'num_terms': 2, 'num_digits': 2, 'expression': '51 * 63'}} -``` -
- -
- -

Chain Sum

-Generate addition/subtraction problems with configurable complexity: -
- -```python -from reasoning_gym.arithmetic import ChainSum, ChainSumConfig - -config = ChainSumConfig( - min_terms=2, # Minimum numbers to add/subtract - max_terms=6, # Maximum numbers - min_digits=1, # Minimum digits per number - max_digits=4, # Maximum digits per number - allow_negation=True, # Allow negative numbers - size=5, # Number of problems - seed=42 # For reproducibility -) - -dataset = ChainSum(config) -for item in dataset: - print(item) -``` - -Example data: - -``` -{ - "question": "426 + 562 =", - "answer": "988", - "metadata": { "num_terms": 2, "num_digits": 3, "expression": "426 + 562" }, -} -{ - "question": "426 + 562 =", - "answer": "988", - "metadata": { "num_terms": 2, "num_digits": 3, "expression": "426 + 562" } -} -``` -
- -
- -

Sequence Completion

-Generate number sequence completion tasks with dynamic pattern generation: -
- -```python -from reasoning_gym.cognition import NumberSequenceDataset, NumberSequenceConfig - -config = NumberSequenceConfig( - min_terms=4, # Minimum visible terms - max_terms=8, # Maximum visible terms - min_value=-100, # Minimum allowed number - max_value=100, # Maximum allowed number - max_complexity=3, # Maximum operations to combine - size=5, # Number of sequences - seed=42 # For reproducibility -) - -dataset = NumberSequenceDataset(config) -for item in dataset: - print(item) -``` - -Example data: - -``` -{ - "question": "3, 6, 12, 24, 48, 96, 192, 384, ?", - "answer": "768", - "metadata": {"rule": "double", "complexity": 3, "sequence": [3, 6, 12, 24, 48, 96, 192, 384, 768]}, -} -{ - "question": "8, 14, 20, 26, 32, 38, 44, ?", - "answer": "50", - "metadata": {"rule": "add 6", "complexity": 1, "sequence": [8, 14, 20, 26, 32, 38, 44, 50]}, -} -``` -
- -
- -

Color Cube Rotation

-Generate 3D spatial reasoning tasks with cube rotations and color tracking: -
- -```python -from reasoning_gym.cognition import ColorCubeRotationDataset, ColorCubeRotationConfig - -config = ColorCubeRotationConfig( - min_rotations=1, # Minimum number of rotations - max_rotations=3, # Maximum number of rotations - size=5, # Number of problems to generate - seed=42 # For reproducibility -) - -dataset = ColorCubeRotationDataset(config) -for item in dataset: - print(item) -``` - -Example data: - -``` -{ - "question": "A cube has:\n- a red top side\n- a blue right side\n- a green front side\n- a yellow left side\n- a white back side\n- an orange bottom side\n\nThe cube is rotated so that the side which was before at the front is now at the top.\nThe cube is rotated so that the side which was before at the right is now at the top.\n\nWhat is now the color of the bottom side of the cube?", - "answer": "yellow", - "metadata": { - "initial_state": {"top": "red", "right": "blue", "front": "green", "left": "yellow", "back": "white", "bottom": "orange"}, - "rotations": ["front", "right"], - "target_side": "bottom", - "num_rotations": 2 - } -} -``` -
- -
- -

Propositional Logic

-Generate logical reasoning tasks with configurable complexity: -
- -```python -from reasoning_gym.logic import PropositionalLogicDataset, PropositionalLogicConfig - -config = PropositionalLogicConfig( - min_vars=2, # Minimum number of variables - max_vars=4, # Maximum number of variables - min_statements=2, # Minimum number of given statements - max_statements=4, # Maximum number of statements - max_complexity=3, # Maximum operator depth - size=5, # Number of problems to generate - seed=42 # For reproducibility -) - -dataset = PropositionalLogicDataset(config) -for item in dataset: - print(item) -``` - -Example data: - -``` -{ - "question": "Given:\n1. R\n2. Q\nWhat can we conclude?", - "answer": "(P ∨ Q)", - "metadata": {"premises": ["R", "Q"], "variables": ["P", "Q", "R", "S"], "complexity": 3}, -} -{ - "question": "Given:\n1. ((Q → P) ∨ (Q → P))\n2. ((Q ↔ Q) → (P → P))\n3. P\nWhat can we conclude?", - "answer": "(P → P)", - "metadata": { - "premises": ["((Q → P) ∨ (Q → P))", "((Q ↔ Q) → (P → P))", "P"], - "variables": ["P", "Q"], - "complexity": 3, - }, -} -``` -
- -
- -

Maze

-Generate a maze with configurable difficulty: -
- -```python -from reasoning_gym.games import MazeConfig, MazeDataset - -config = MazeConfig( - min_dist=3, - max_dist=5, - min_grid_size=5, - max_grid_size=5, - size=2, - seed=4, -) - -dataset = MazeDataset(config) - -for item in dataset: - print() - print(item["question"]) - print(item) -``` - -Example data: - -``` -Navigate from 'd' (start) to '}' (goal): - -uuuuu -uCCdu -uCCCu -uu}Cu -uuuuu -Legend: 'u' = Wall, 'C' = Path - -{'question': "Navigate from 'd' (start) to '}' (goal):\n\nuuuuu\nuCCdu\nuCCCu\nuu}Cu\nuuuuu\nLegend: 'u' = Wall, 'C' = Path\n", 'answer': '3', 'metadata': {'grid_size': 5, 'grid': ['uuuuu', 'uCCdu', 'uCCCu', 'uu}Cu', 'uuuuu'], 'shortest_path_length': 3, 'start': 'd', 'goal': '}', 'wall': 'u', 'path': 'C'}} - -Navigate from 'J' (start) to '_' (goal): - -<<<<< -< +- `NQueensDataset`: Generate N-Queens puzzles with configurable board size and number of starting queens ## Future Generator Ideas @@ -403,4 +138,4 @@ Legend: '<' = Wall, 'w' = Path ## Call for Contributions -If you have ideas for additional procedural dataset generators please create an issue here or contact us in the `#arc-agi-2` channel of the [GPU-Mode discord server](https://discord.gg/gpumode). +If you have ideas for additional procedural dataset generators please create an issue here or contact us in the `#reasoning-gym` channel of the [GPU-Mode discord server](https://discord.gg/gpumode). diff --git a/examples/veRL/.gitignore b/examples/veRL/.gitignore new file mode 100644 index 00000000..c54a47c0 --- /dev/null +++ b/examples/veRL/.gitignore @@ -0,0 +1,3 @@ +outputs/ +wandb/ +verl_output.log diff --git a/examples/veRL/README.md b/examples/veRL/README.md new file mode 100644 index 00000000..e2ec6e50 --- /dev/null +++ b/examples/veRL/README.md @@ -0,0 +1,19 @@ +### env setup + +``` +conda create --name verl python=3.12 -y +conda activate verl + +pip install flash-attn --no-build-isolation +pip install vllm==0.7.0 ray wandb +``` + +### clone and install veRL + +tested with verl HEAD a65c9157bc0b85b64cd753de19f94e80a11bd871 + +``` +git clone https://github.com/volcengine/verl.git +cd verl +pip install -e . +``` diff --git a/examples/veRL/config/ppo_trainer.yaml b/examples/veRL/config/ppo_trainer.yaml new file mode 100644 index 00000000..b294a7cb --- /dev/null +++ b/examples/veRL/config/ppo_trainer.yaml @@ -0,0 +1,167 @@ +data: + tokenizer: null + train_files: ~/data/rlhf/gsm8k/train.parquet + val_files: ~/data/rlhf/gsm8k/test.parquet + prompt_key: prompt + max_prompt_length: 512 + max_response_length: 512 + train_batch_size: 1024 + val_batch_size: 1312 + return_raw_input_ids: False # This should be set to true when the tokenizer between policy and rm differs + return_raw_chat: False + +actor_rollout_ref: + hybrid_engine: True + model: + path: ~/models/deepseek-llm-7b-chat + external_lib: null + override_config: { } + enable_gradient_checkpointing: True + use_remove_padding: False + actor: + strategy: fsdp # This is for backward-compatibility + ppo_mini_batch_size: 256 + ppo_micro_batch_size: null # will be deprecated, use ppo_micro_batch_size_per_gpu + ppo_micro_batch_size_per_gpu: null + use_dynamic_bsz: False + ppo_max_token_len_per_gpu: 16384 # n * ${data.max_prompt_length} + ${data.max_response_length} + grad_clip: 1.0 + clip_ratio: 0.2 + entropy_coeff: 0.001 + use_kl_loss: False # True for GRPO + kl_loss_coef: 0.001 # for grpo + kl_loss_type: low_var_kl # for grpo + ppo_epochs: 1 + shuffle: False + ulysses_sequence_parallel_size: 1 # sp size + optim: + lr: 1e-6 + lr_warmup_steps_ratio: 0. # the total steps will be injected during runtime + min_lr_ratio: null # only useful for warmup with cosine + warmup_style: constant # select from constant/cosine + total_training_steps: -1 # must be override by program + fsdp_config: + wrap_policy: + # transformer_layer_cls_to_wrap: None + min_num_params: 0 + param_offload: False + grad_offload: False + optimizer_offload: False + fsdp_size: -1 + ref: + fsdp_config: + param_offload: False + wrap_policy: + # transformer_layer_cls_to_wrap: None + min_num_params: 0 + log_prob_micro_batch_size: null # will be deprecated, use log_prob_micro_batch_size_per_gpu + log_prob_micro_batch_size_per_gpu: null + log_prob_use_dynamic_bsz: ${actor_rollout_ref.actor.use_dynamic_bsz} + log_prob_max_token_len_per_gpu: ${actor_rollout_ref.actor.ppo_max_token_len_per_gpu} + ulysses_sequence_parallel_size: ${actor_rollout_ref.actor.ulysses_sequence_parallel_size} # sp size + rollout: + name: vllm + temperature: 1.0 + top_k: -1 # 0 for hf rollout, -1 for vllm rollout + top_p: 1 + prompt_length: ${data.max_prompt_length} # not use for opensource + response_length: ${data.max_response_length} + # for vllm rollout + dtype: bfloat16 # should align with FSDP + gpu_memory_utilization: 0.5 + ignore_eos: False + enforce_eager: True + free_cache_engine: True + load_format: dummy_dtensor + tensor_model_parallel_size: 2 + max_num_batched_tokens: 8192 + max_num_seqs: 1024 + log_prob_micro_batch_size: null # will be deprecated, use log_prob_micro_batch_size_per_gpu + log_prob_micro_batch_size_per_gpu: null + log_prob_use_dynamic_bsz: ${actor_rollout_ref.actor.use_dynamic_bsz} + log_prob_max_token_len_per_gpu: ${actor_rollout_ref.actor.ppo_max_token_len_per_gpu} + disable_log_stats: True + enable_chunked_prefill: True # could get higher throughput + # for hf rollout + do_sample: True + # number of responses (i.e. num sample times) + n: 1 # > 1 for grpo + +critic: + strategy: fsdp + optim: + lr: 1e-5 + lr_warmup_steps_ratio: 0. # the total steps will be injected during runtime + min_lr_ratio: null # only useful for warmup with cosine + warmup_style: constant # select from constant/cosine + total_training_steps: -1 # must be override by program + model: + path: ~/models/deepseek-llm-7b-chat + tokenizer_path: ${actor_rollout_ref.model.path} + override_config: { } + external_lib: ${actor_rollout_ref.model.external_lib} + enable_gradient_checkpointing: True + use_remove_padding: False + fsdp_config: + param_offload: False + grad_offload: False + optimizer_offload: False + wrap_policy: + # transformer_layer_cls_to_wrap: None + min_num_params: 0 + fsdp_size: -1 + ppo_mini_batch_size: ${actor_rollout_ref.actor.ppo_mini_batch_size} + ppo_micro_batch_size: null # will be deprecated, use ppo_micro_batch_size_per_gpu + ppo_micro_batch_size_per_gpu: null + forward_micro_batch_size: ${critic.ppo_micro_batch_size} + forward_micro_batch_size_per_gpu: ${critic.ppo_micro_batch_size_per_gpu} + use_dynamic_bsz: ${actor_rollout_ref.actor.use_dynamic_bsz} + ppo_max_token_len_per_gpu: 32768 # (${actor_rollout_ref.actor.ppo_max_token_len_per_gpu}) * 2 + forward_max_token_len_per_gpu: ${critic.ppo_max_token_len_per_gpu} + ulysses_sequence_parallel_size: 1 # sp size + ppo_epochs: ${actor_rollout_ref.actor.ppo_epochs} + shuffle: ${actor_rollout_ref.actor.shuffle} + grad_clip: 1.0 + cliprange_value: 0.5 + +reward_model: + enable: False + strategy: fsdp + model: + input_tokenizer: ${actor_rollout_ref.model.path} # set this to null if the chat template is identical + path: ~/models/FsfairX-LLaMA3-RM-v0.1 + external_lib: ${actor_rollout_ref.model.external_lib} + use_remove_padding: False + fsdp_config: + min_num_params: 0 + param_offload: False + fsdp_size: -1 + micro_batch_size: null # will be deprecated, use micro_batch_size_per_gpu + micro_batch_size_per_gpu: null # set a number + max_length: null + ulysses_sequence_parallel_size: 1 # sp size + use_dynamic_bsz: ${critic.use_dynamic_bsz} + forward_max_token_len_per_gpu: ${critic.forward_max_token_len_per_gpu} + +algorithm: + gamma: 1.0 + lam: 1.0 + adv_estimator: gae + kl_penalty: kl # how to estimate kl divergence + kl_ctrl: + type: fixed + kl_coef: 0.001 + +trainer: + total_epochs: 30 + total_training_steps: null + project_name: verl_examples + experiment_name: gsm8k + logger: [ 'console', 'wandb' ] + nnodes: 1 + n_gpus_per_node: 8 + save_freq: -1 + test_freq: -1 + critic_warmup: 0 + default_hdfs_dir: ~/experiments/gsm8k/ppo/${trainer.experiment_name} + default_local_dir: checkpoints/${trainer.project_name}/${trainer.experiment_name} diff --git a/examples/veRL/launch_on_4gpu.sh b/examples/veRL/launch_on_4gpu.sh new file mode 100755 index 00000000..0a51f68c --- /dev/null +++ b/examples/veRL/launch_on_4gpu.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +export N_GPUS=4 +export BASE_MODEL=meta-llama/Llama-3.2-1B-Instruct +export ROLLOUT_TP_SIZE=2 +export EXPERIMENT_NAME=chain_sum_llama +export VLLM_ATTENTION_BACKEND=XFORMERS + +bash ./train.sh diff --git a/examples/veRL/main_ppo_custom_reward.py b/examples/veRL/main_ppo_custom_reward.py new file mode 100644 index 00000000..2addb8e9 --- /dev/null +++ b/examples/veRL/main_ppo_custom_reward.py @@ -0,0 +1,285 @@ +# This example is an adapted version of Bytedance's code: +# https://github.com/volcengine/verl/blob/a65c9157bc0b85b64cd753de19f94e80a11bd871/verl/trainer/main_ppo.py +from typing import Optional + +import hydra +import ray +import torch +import verl.utils.torch_functional as verl_F +from omegaconf import OmegaConf, open_dict +from torch.utils.data import DataLoader, Dataset +from transformers import PreTrainedTokenizer +from verl import DataProto +from verl.trainer.ppo.ray_trainer import RayPPOTrainer +from verl.utils.dataset.rl_dataset import collate_fn +from verl.utils.model import compute_position_id_with_mask + +import reasoning_gym +import reasoning_gym.utils +from reasoning_gym.utils import extract_answer + + +class ReasoningGymDataset(Dataset): + def __init__( + self, + tokenizer: PreTrainedTokenizer, + dataset_name: str, + seed: int, + size: int, + developer_prompt: Optional[str] = None, + developer_role: str = "system", + max_prompt_length: int = 2048, + truncation: str = "error", ## ['left', 'right', 'error'] + return_raw_chat: bool = False, + ): + self.tokenizer = tokenizer + self.dataset_name = dataset_name + self.data = reasoning_gym.create_dataset(dataset_name, seed=seed, size=size) + self.developer_prompt = developer_prompt + self.developer_role = developer_role + self.max_prompt_length = max_prompt_length + self.truncation = truncation + self.return_raw_chat = return_raw_chat + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, index): + row_dict = self.data[index].copy() + q = row_dict["question"] + + chat = [] + if self.developer_prompt is not None: + chat.append({"role": self.developer_role, "content": self.developer_prompt}) + chat.append({"role": "user", "content": q}) + + prompt = self.tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True) + + input_ids, attention_mask = verl_F.tokenize_and_postprocess_data( + prompt=prompt, + tokenizer=self.tokenizer, + max_length=self.max_prompt_length, + pad_token_id=self.tokenizer.pad_token_id, + left_pad=True, + truncation=self.truncation, + ) + + position_ids = compute_position_id_with_mask(attention_mask) + + row_dict["data_source"] = "reasoning_gym/" + self.dataset_name + row_dict["input_ids"] = input_ids[0] + row_dict["attention_mask"] = attention_mask[0] + row_dict["position_ids"] = position_ids[0] + + # encode prompts without chat template + if self.return_raw_chat: + row_dict["raw_prompt"] = chat.tolist() + + # add index for each prompt + # index = row_dict.get("extra_info", {}).get("index", 0) + row_dict["index"] = index + + return row_dict + + +class RayPPOTrainerCustom(RayPPOTrainer): + def __init__( + self, + config, + tokenizer, + role_worker_mapping: dict, + resource_pool_manager, + ray_worker_group_cls, + dataset_name: str = "chain_sum", + dataset_size: int = 10000, + ): + self.dataset_name = dataset_name + self.dataset_size = dataset_size + + developer_prompt = reasoning_gym.utils.SYSTEM_PROMPTS["DeepSeekZero"] + self.train_dataset = ReasoningGymDataset( + tokenizer=tokenizer, + dataset_name=self.dataset_name, + seed=1, + size=self.dataset_size, + developer_prompt=developer_prompt, + ) + + self.val_dataset = ReasoningGymDataset( + tokenizer=tokenizer, + dataset_name=self.dataset_name, + seed=2, + size=self.dataset_size, + developer_prompt=developer_prompt, + ) + + train_reward_fn = lambda data: self._score_output(data, num_examine=0) + val_reward_fn = lambda data: self._score_output(data, num_examine=1) + + super().__init__( + config, + tokenizer, + role_worker_mapping, + resource_pool_manager, + ray_worker_group_cls, + train_reward_fn, + val_reward_fn, + ) + + def _score_output(self, data: DataProto, num_examine: int = 0) -> torch.Tensor: + reward_tensor = torch.zeros_like(data.batch["responses"], dtype=torch.float32) + + num_printed = 0 + for i in range(len(data)): + data_item = data[i] # DataProtoItem + + prompt_ids = data_item.batch["prompts"] # tokenized prompts + prompt_length = prompt_ids.shape[-1] + + valid_prompt_length = data_item.batch["attention_mask"][:prompt_length].sum() + valid_prompt_ids = prompt_ids[-valid_prompt_length:] + + response_ids = data_item.batch["responses"] + valid_response_length = data_item.batch["attention_mask"][prompt_length:].sum() + valid_response_ids = response_ids[:valid_response_length] + + # decode + sequences = torch.cat((valid_prompt_ids, valid_response_ids)) + sequences_str = self.tokenizer.decode(sequences) + + index = data_item.non_tensor_batch["index"] + + score = self._compute_score( + solution_str=sequences_str, + index=index, + ) + reward_tensor[i, valid_response_length - 1] = score + + if num_printed < num_examine: + print(f"reward={score}, seq={sequences_str}") + num_printed += 1 + + return reward_tensor + + def _compute_score(self, solution_str: str, index: int) -> float: + found_answer = extract_answer(solution_str, tag_name="answer") + entry = self.train_dataset.data[index] + reward = self.train_dataset.data.score_answer(found_answer, entry=entry) + # print(f"found answer={found_answer}; reward: {reward};") + return reward + + def _create_dataloader(self): + self.train_dataloader = DataLoader( + dataset=self.train_dataset, + batch_size=self.config.data.train_batch_size, + shuffle=True, + drop_last=True, + collate_fn=collate_fn, + ) + + self.val_dataloader = DataLoader( + dataset=self.val_dataset, + batch_size=len(self.val_dataset), + shuffle=True, + drop_last=True, + collate_fn=collate_fn, + ) + + assert len(self.train_dataloader) >= 1 + assert len(self.val_dataloader) >= 1 + + print(f"Size of train dataloader: {len(self.train_dataloader)}") + print(f"Size of val dataloader: {len(self.val_dataloader)}") + + # inject total_training_steps to actor/critic optim_config. This is hacky. + total_training_steps = len(self.train_dataloader) * self.config.trainer.total_epochs + + if self.config.trainer.total_training_steps is not None: + total_training_steps = self.config.trainer.total_training_steps + + self.total_training_steps = total_training_steps + print(f"Total training steps: {self.total_training_steps}") + + OmegaConf.set_struct(self.config, True) + with open_dict(self.config): + self.config.actor_rollout_ref.actor.optim.total_training_steps = total_training_steps + self.config.critic.optim.total_training_steps = total_training_steps + + +@ray.remote +def main_task(config): + # print initial config + from pprint import pprint + + from verl.utils import hf_tokenizer + from verl.utils.fs import copy_local_path_from_hdfs + + pprint(OmegaConf.to_container(config, resolve=True)) # resolve=True will eval symbol values + OmegaConf.resolve(config) + + # download the checkpoint from hdfs + local_path = copy_local_path_from_hdfs(config.actor_rollout_ref.model.path) + + # instantiate tokenizer + tokenizer = hf_tokenizer(local_path) + + # define worker classes + if config.actor_rollout_ref.actor.strategy == "fsdp": + assert config.actor_rollout_ref.actor.strategy == config.critic.strategy + from verl.single_controller.ray import RayWorkerGroup + from verl.workers.fsdp_workers import ActorRolloutRefWorker, CriticWorker + + ray_worker_group_cls = RayWorkerGroup + + elif config.actor_rollout_ref.actor.strategy == "megatron": + assert config.actor_rollout_ref.actor.strategy == config.critic.strategy + from verl.single_controller.ray.megatron import NVMegatronRayWorkerGroup + from verl.workers.megatron_workers import ActorRolloutRefWorker, CriticWorker + + ray_worker_group_cls = NVMegatronRayWorkerGroup + + else: + raise NotImplementedError + + from verl.trainer.ppo.ray_trainer import ResourcePoolManager, Role + + role_worker_mapping = { + Role.ActorRollout: ray.remote(ActorRolloutRefWorker), + Role.Critic: ray.remote(CriticWorker), + Role.RefPolicy: ray.remote(ActorRolloutRefWorker), + } + + global_pool_id = "global_pool" + resource_pool_spec = { + global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes, + } + mapping = { + Role.ActorRollout: global_pool_id, + Role.Critic: global_pool_id, + Role.RefPolicy: global_pool_id, + } + + resource_pool_manager = ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping) + + trainer = RayPPOTrainerCustom( + config=config, + tokenizer=tokenizer, + role_worker_mapping=role_worker_mapping, + resource_pool_manager=resource_pool_manager, + ray_worker_group_cls=ray_worker_group_cls, + ) + trainer.init_workers() + trainer.fit() + + +@hydra.main(config_path="config", config_name="ppo_trainer", version_base=None) +def main(config): + if not ray.is_initialized(): + # this is for local ray cluster + ray.init(runtime_env={"env_vars": {"TOKENIZERS_PARALLELISM": "true", "NCCL_DEBUG": "WARN"}}) + + ray.get(main_task.remote(config)) + + +if __name__ == "__main__": + main() diff --git a/examples/veRL/train.sh b/examples/veRL/train.sh new file mode 100755 index 00000000..92ed0b84 --- /dev/null +++ b/examples/veRL/train.sh @@ -0,0 +1,30 @@ +#!/bin/bash +python3 -u main_ppo_custom_reward.py \ +data.train_files=$DATA_DIR/train.parquet \ +data.val_files=$DATA_DIR/test.parquet \ +data.train_batch_size=256 \ +data.val_batch_size=1312 \ +data.max_prompt_length=256 \ +data.max_response_length=1024 \ +actor_rollout_ref.model.path=$BASE_MODEL \ +actor_rollout_ref.actor.optim.lr=1e-6 \ +actor_rollout_ref.actor.ppo_mini_batch_size=128 \ +actor_rollout_ref.actor.ppo_micro_batch_size=8 \ +actor_rollout_ref.rollout.log_prob_micro_batch_size=8 \ +actor_rollout_ref.rollout.tensor_model_parallel_size=$ROLLOUT_TP_SIZE \ +actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ +actor_rollout_ref.ref.log_prob_micro_batch_size=4 \ +critic.optim.lr=1e-5 \ +critic.model.path=$BASE_MODEL \ +critic.ppo_micro_batch_size=8 \ +algorithm.kl_ctrl.kl_coef=0.001 \ +trainer.logger=['wandb'] \ ++trainer.val_before_train=False \ +trainer.default_hdfs_dir=null \ +trainer.n_gpus_per_node=$N_GPUS \ +trainer.nnodes=1 \ +trainer.save_freq=100 \ +trainer.test_freq=100 \ +trainer.project_name=verl_chain_sum \ +trainer.experiment_name=$EXPERIMENT_NAME \ +trainer.total_epochs=15 2>&1 | tee verl_output.log diff --git a/pyproject.toml b/pyproject.toml index b86e3c5d..964c0b4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,21 @@ build-backend = "hatchling.build" [project] name = "reasoning_gym" -version = "0.1.1" +version = "0.1.3" authors = [ { name = "Open-Thought community", email = "andreas.koepf@xamla.com" }, ] description = "A library of procedural dataset generators for training reasoning models" readme = "README.md" requires-python = ">=3.11" -dependencies = ["sympy>=1.13.1", "magiccube==0.3.0", "pyfiglet==1.0.2"] +dependencies = [ + "bfi==1.0.4", + "cellpylib==2.4.0", + "sympy>=1.13.1", + "magiccube==0.3.0", + "pyfiglet==1.0.2", + "pytz>=2024.1" +] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", @@ -20,6 +27,12 @@ classifiers = [ license = "Apache-2.0" license-files = ["LICENSE*"] +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", +] + [project.urls] "Homepage" = "https://github.com/open-thought/reasoning-gym" "Bug Tracker" = "https://github.com/open-thought/reasoning-gym/issues" @@ -39,3 +52,9 @@ include = '\.pyi?$' profile = "black" multi_line_output = 3 line_length = 120 + +[tool.pytest.ini_options] +addopts = "-ra -q" +testpaths = [ + "tests", +] diff --git a/reasoning_gym/__init__.py b/reasoning_gym/__init__.py index b25eb134..054cbd95 100644 --- a/reasoning_gym/__init__.py +++ b/reasoning_gym/__init__.py @@ -2,17 +2,18 @@ Reasoning Gym - A library of procedural dataset generators for training reasoning models """ -from . import algebra, algorithmic, arithmetic, cognition, data, games, graphs, logic +from . import algebra, algorithmic, arithmetic, cognition, data, games, geometry, graphs, logic from .factory import create_dataset, register_dataset -__version__ = "0.1.1" +__version__ = "0.1.3" __all__ = [ - "arithmetic", - "algorithmic", "algebra", + "algorithmic", + "arithmetic", "cognition", "data", "games", + "geometry", "graphs", "logic", "create_dataset", diff --git a/reasoning_gym/algebra/__init__.py b/reasoning_gym/algebra/__init__.py index 69d4b91e..fc7a867a 100644 --- a/reasoning_gym/algebra/__init__.py +++ b/reasoning_gym/algebra/__init__.py @@ -1,9 +1,15 @@ +from .intermediate_integration import IntermediateIntegrationConfig, IntermediateIntegrationDataset from .polynomial_equations import PolynomialEquationsConfig, PolynomialEquationsDataset from .simple_equations import SimpleEquationsConfig, SimpleEquationsDataset +from .simple_integration import SimpleIntegrationConfig, SimpleIntegrationDataset __all__ = [ - "SimpleEquationsDataset", - "SimpleEquationsConfig", + "IntermediateIntegrationConfig", + "IntermediateIntegrationDataset", "PolynomialEquationsConfig", "PolynomialEquationsDataset", + "SimpleEquationsDataset", + "SimpleEquationsConfig", + "SimpleIntegrationConfig", + "SimpleIntegrationDataset", ] diff --git a/reasoning_gym/algebra/intermediate_integration.py b/reasoning_gym/algebra/intermediate_integration.py new file mode 100644 index 00000000..5d0b139c --- /dev/null +++ b/reasoning_gym/algebra/intermediate_integration.py @@ -0,0 +1,263 @@ +import random +from dataclasses import dataclass +from typing import Any, Dict, Optional + +import sympy + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class IntermediateIntegrationConfig: + problem_types: tuple = ("substitution", "by_parts") + substitution_types: tuple = ( + "linear", # (ax + b)^n + "trigonometric", # sin**2(x)cos(x) + "exponential", # 2xe^x**2 + "radical", # x (3x + 2)^1/2 + ) + + # Integration by parts problem categories + by_parts_types: tuple = ( + "polynomial_exp_trig", # e.g. x^2*e^x + "log_inverse_trig", # e.g. ln(x)/arctan(x) + "cyclic", # e.g. e^x*sinx requiring cyclic integration + "repeated_parts", # Requires multiple integration by parts + ) + seed: Optional[int] = None + size: int = 500 + + linear_lower_bound: int = 1 # coefficient of linear expression + linear_upper_bound: int = 10 + min_linear_degree: int = 2 # degree of linear expression in substitution problem + max_linear_degree: int = 4 + outer_constant_min: int = 1 # multiplicative constant to multiply integrand by + outer_constant_max: int = 3 + min_poly_degree: int = 1 # degree of polynomial in by parts problem + max_poly_degree: int = 3 + symbols: tuple = ("x", "X") + operators: tuple = ( + "+", + "-", + ) + + def validate(self) -> None: + """Validate the configuration parameters of the integral problem""" + assert self.size > 0, "size must be positive" + assert self.linear_lower_bound > 0, "linear_lower_bound must be positive" + assert self.linear_upper_bound >= self.linear_lower_bound, "linear_upper_bound must be >= linear_lower_bound" + assert self.min_linear_degree > 0, "min_linear_degree must be positive" + assert self.max_linear_degree >= self.min_linear_degree, "max_linear_degree must be >= min_linear_degree" + assert self.outer_constant_min > 0, "outer_constant_min must be positive" + assert self.outer_constant_max >= self.outer_constant_min, "outer_constant_max must be >= outer_constant_min" + assert self.min_poly_degree > 0, "min_poly_degree must be positive" + assert self.max_poly_degree >= self.min_poly_degree, "max_poly_degree must be >= min_poly_degree" + assert all(op in ("+", "-") for op in self.operators), "invalid operator specified" + assert all(symbols in ("x", "X") for symbols in self.symbols), "invalid symbol specified" + assert all(t in ("substitution", "by_parts") for t in self.problem_types), "invalid problem type" + assert all( + t in ("linear", "trigonometric", "exponential", "radical") for t in self.substitution_types + ), "invalid substitution type" + assert all( + t in ("polynomial_exp_trig", "log_inverse_trig", "cyclic", "repeated_parts") for t in self.by_parts_types + ), "invalid by_parts type" + + +class IntermediateIntegrationDataset(ProceduralDataset): + """Generates intermediate integration problem - either + by substitution or by parts""" + + """Add multiplicative constant""" + + def __init__(self, config: IntermediateIntegrationConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + self.prompt_template = [ + "Find the indefinite integral: ∫ {integrand} dx", + "Calculate the antiderivative: ∫ {integrand} dx", + "Evaluate the indefinite integral: ∫ {integrand} dx", + ] + + def _get_outer_constant(self, rng: random.Random) -> int: + """Helper to generate signed outer constant from config""" + value = rng.randint(self.config.outer_constant_min, self.config.outer_constant_max) + return -value if rng.choice(self.config.operators) == "-" else value + + def _generate_linear_substitution_problem(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate a linear substitution problem with outer constant""" + a = rng.randint(self.config.linear_lower_bound, self.config.linear_upper_bound) + b = rng.randint(self.config.linear_lower_bound, self.config.linear_upper_bound) + + linear_function = a * x + (b if rng.choice(self.config.operators) == "+" else -b) + degree = rng.randint(self.config.min_linear_degree, self.config.max_linear_degree) + + return self._get_outer_constant(rng) * linear_function**degree + + def _generate_exponential_substitution(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate exponential substitution problem with outer constant""" + exponent_type = rng.choice(["linear", "quadratic"]) + + # Generate terms with signs + num_terms = 2 if exponent_type == "linear" else 3 + terms = [ + (-1 if rng.choice(self.config.operators) == "-" else 1) + * rng.randint(self.config.linear_lower_bound, self.config.linear_upper_bound) + for _ in range(num_terms) + ] + + if exponent_type == "linear": + u = terms[0] * x + terms[1] + du_dx = terms[0] + else: # Quadratic + u = terms[0] * x**2 + terms[1] * x + terms[2] + du_dx = 2 * terms[0] * x + terms[1] + + return self._get_outer_constant(rng) * du_dx * sympy.exp(u) + + def _generate_radical_substitution(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate radical substitution problem with outer constant""" + + # Generate linear expression under radical: ax + b with possible negative coefficients + a = (-1 if rng.choice(self.config.operators) == "-" else 1) * rng.randint( + self.config.linear_lower_bound, self.config.linear_upper_bound + ) + b = (-1 if rng.choice(self.config.operators) == "-" else 1) * rng.randint( + self.config.linear_lower_bound, self.config.linear_upper_bound + ) + + u = a * x + b + derivative = a # du/dx + + integrand = derivative * sympy.sqrt(u) + return self._get_outer_constant(rng) * integrand + + def _generate_trigonometric_substitution(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate trigonometric substitution with outer constant""" + trig_func = rng.choice(["sin", "cos"]) + + # Generate signed coefficients + a = (-1 if rng.choice(self.config.operators) == "-" else 1) * rng.randint( + self.config.linear_lower_bound, self.config.linear_upper_bound + ) + b = (-1 if rng.choice(self.config.operators) == "-" else 1) * rng.randint( + self.config.linear_lower_bound, self.config.linear_upper_bound + ) + + inner = a * x + b + power = rng.randint(1, 4) + if trig_func == "sin": + integrand = a * sympy.cos(inner) * sympy.sin(inner) ** power + else: + integrand = -a * sympy.sin(inner) * sympy.cos(inner) ** power + return self._get_outer_constant(rng) * integrand + + def _generate_polynomial_exp_trig(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate polynomial × exponential/trigonometric integrand""" + poly_degree = rng.randint(self.config.min_poly_degree, self.config.max_poly_degree) + + func_type = rng.choice(["exp", "sin", "cos"]) + if func_type == "exp": + transcendental = sympy.exp(x) + else: + coefficient = rng.randint(1, 3) + transcendental = sympy.Function(func_type)(coefficient * x) + + polynomial = x**poly_degree + integrand = polynomial * transcendental + return self._get_outer_constant(rng) * integrand + + def _generate_log_inverse_trig(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate logarithmic or inverse trigonometric integrand""" + func_type = rng.choice(["log", "asin", "atan"]) + + if func_type == "log": + log_arg = x if rng.random() < 0.8 else x ** rng.randint(2, 3) + func = sympy.ln(log_arg) + else: + coefficient = rng.randint(1, 3) + func = sympy.Function(func_type)(coefficient * x) + + return self._get_outer_constant(rng) * func + + def _generate_cyclic_integral(self, rng: random.Random, x: sympy.Symbol) -> sympy.Expr: + """Generate cyclic integral (e.g., e^x * sinx)""" + func_pair = rng.choice( + [(sympy.exp(x), sympy.sin(x)), (sympy.exp(x), sympy.cos(x)), (sympy.sin(x), sympy.cos(x))] + ) + integrand = func_pair[0] * func_pair[1] + return self._get_outer_constant(rng) * integrand + + def _generate_repeated_parts(self, rng: random.Random, x: sympy.Symbol): + """Generate problem requiring multiple integration by parts""" + poly_degree = rng.randint(3, self.config.max_poly_degree) + transcendental = rng.choice([sympy.sin(x), sympy.cos(x), sympy.exp(x)]) + integrand = x**poly_degree * transcendental + return self._get_outer_constant(rng) * integrand + + def __getitem__(self, index: int): + """Generate either substitution or by-parts problem""" + rng = random.Random(self.seed + index) + problem_type = rng.choice(self.config.problem_types) + x = sympy.Symbol(rng.choice(self.config.symbols)) + + if problem_type == "substitution": + substitution_type = rng.choice(self.config.substitution_types) + if substitution_type == "linear": + integrand = self._generate_linear_substitution_problem(rng, x) + elif substitution_type == "trigonometric": + integrand = self._generate_trigonometric_substitution(rng, x) + elif substitution_type == "exponential": + integrand = self._generate_exponential_substitution(rng, x) + elif substitution_type == "radical": + integrand = self._generate_radical_substitution(rng, x) + else: + parts_type = rng.choice(self.config.by_parts_types) + if parts_type == "polynomial_exp_trig": + integrand = self._generate_polynomial_exp_trig(rng, x) + elif parts_type == "log_inverse_trig": + integrand = self._generate_log_inverse_trig(rng, x) + elif parts_type == "cyclic": + integrand = self._generate_cyclic_integral(rng, x) + elif parts_type == "repeated_parts": + integrand = self._generate_repeated_parts(rng, x) + + answer = sympy.integrate(integrand, x) + answer_str = str(answer) + " + C" + + return { + "question": rng.choice(self.prompt_template).format(integrand=integrand), + "answer": answer_str, + "metadata": { + "integrand": str(integrand), + "problem_type": problem_type, + "variable": str(x), + "type": substitution_type if problem_type == "substitution" else parts_type, + "expected_answer_expression": answer, + }, + } + + def score_answer(self, answer: Optional[str], metadata: Dict[str, Any]) -> float: + """Determine if the solution provided solves the problem""" + reward = 0.0 + if answer is not None: + try: + var = metadata["variable"] + x = sympy.Symbol(var) + # Parse answer while allowing integration constant 'C' + user_expr = sympy.parse_expr(answer, local_dict={var: x, "C": sympy.Symbol("C")}) + # Compute derivative of student's answer + derivative = sympy.diff(user_expr, x) + integrand = sympy.parse_expr(metadata["integrand"], local_dict={var: x}) + + # Check mathematical equivalence through simplification + if sympy.simplify(derivative - integrand) == 0: + reward = 1.0 + elif answer.strip(): + reward = 0.05 + else: + reward = 0.01 + except: + reward = 0.01 + return reward + + +register_dataset("intermediate_integration", IntermediateIntegrationDataset, IntermediateIntegrationConfig) diff --git a/reasoning_gym/algebra/simple_integration.py b/reasoning_gym/algebra/simple_integration.py new file mode 100644 index 00000000..1da32004 --- /dev/null +++ b/reasoning_gym/algebra/simple_integration.py @@ -0,0 +1,108 @@ +import random +from dataclasses import dataclass +from fractions import Fraction +from typing import Any, Dict, Optional + +import sympy + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class SimpleIntegrationConfig: + min_terms: int = 2 + max_terms: int = 5 + min_degree: int = 1 + max_degree: int = 10 + min_bounds: int = 1 + max_bounds: int = 10 + operators: tuple = ("+", "-") + symbols: tuple = ("x", "X") + seed: Optional[int] = None + size: int = 500 + + def validate(self) -> None: + """Validate the configuration parameters of the integral proble""" + assert self.min_bounds > 0, "min_bounds must be positive" + assert self.max_bounds >= self.min_bounds, "max_bounds must be >= min_bounds" + assert self.min_terms >= 0, "min_terms must be positive" + assert self.max_terms >= self.min_terms, "max_terms must be >= min_terms" + assert self.min_degree >= -10, "min_degree must be >= -10" + assert self.max_degree >= self.min_degree, "max_degree must be >= min_degree" + assert all(op in ("+", "-") for op in self.operators), "invalid operator specified" + + +class SimpleIntegrationDataset(ProceduralDataset): + """Generates simple integration problems with one variable""" + + def __init__(self, config: SimpleIntegrationConfig): + self._prompt_templates = [ + "Find the indefinite integral: ∫ {integrand} dx", + "Calculate the antiderivative: ∫ {integrand} dx", + "Evaluate the indefinite integral: ∫ {integrand} dx", + ] + super().__init__(config=config, seed=config.seed, size=config.size) + + def _generate_coefficient(self, rng: random.Random) -> Fraction: + """Generate a random coefficient for the polynomial""" + if rng.choice([True, False]): # 50% chance for integer + return Fraction(rng.randint(self.config.min_bounds, self.config.max_bounds), 1) + denominator = rng.randint(2, 10) + return Fraction(rng.randint(self.config.min_bounds, self.config.max_bounds), denominator) + + def _generate_polynomial(self, rng: random.Random) -> tuple[sympy.Symbol, sympy.Expr]: + """Generate a random polynomial with one variable""" + terms = [] + x = sympy.Symbol(rng.choice(self.config.symbols)) + + for _ in range(rng.randint(self.config.min_terms, self.config.max_terms)): + coefficient = self._generate_coefficient(rng) + degree = rng.randint(self.config.min_degree, self.config.max_degree) + operator = rng.choice(self.config.operators) + term = coefficient * x**degree + if operator == "-": + term = -term + terms.append(term) + return x, sum(terms) + + def __getitem__(self, idx: int) -> dict: + rng = random.Random(self.seed + idx) + symbol, polynomial = self._generate_polynomial(rng) + derivative = sympy.diff(polynomial, symbol) + + return { + "question": rng.choice(self._prompt_templates).format(integrand=derivative), + "answer": str(polynomial) + " + C", + "metadata": { + "integrand": str(derivative), + "variable": str(symbol), + "expected_answer_expression": polynomial, + }, + } + + def score_answer(self, answer: Optional[str], metadata: Dict[str, Any]) -> float: + """Determine if the solution provided solves the problem""" + reward = 0.0 + if answer is not None: + try: + var = metadata["variable"] + x = sympy.Symbol(var) + # Parse answer while allowing integration constant 'C' + user_expr = sympy.parse_expr(answer, local_dict={var: x, "C": sympy.Symbol("C")}) + # Compute derivative of student's answer + derivative = sympy.diff(user_expr, x) + integrand = sympy.parse_expr(metadata["integrand"], local_dict={var: x}) + + # Check mathematical equivalence through simplification + if sympy.simplify(derivative - integrand) == 0: + reward = 1.0 + elif answer.strip(): + reward = 0.05 + else: + reward = 0.01 + except: + reward = 0.01 + return reward + + +register_dataset("simple_integration", SimpleIntegrationDataset, SimpleIntegrationConfig) diff --git a/reasoning_gym/algorithmic/__init__.py b/reasoning_gym/algorithmic/__init__.py index 5345eaec..1b509970 100644 --- a/reasoning_gym/algorithmic/__init__.py +++ b/reasoning_gym/algorithmic/__init__.py @@ -14,9 +14,9 @@ from .number_sorting import NumberSortingConfig, NumberSortingDataset from .sentence_reordering import SentenceReorderingConfig, SentenceReorderingDataset from .spell_backward import SpellBackwardConfig, SpellBackwardDataset +from .word_ladder import WordLadderConfig, WordLadderDataset from .word_sequence_reversal import WordSequenceReversalConfig, WordSequenceReversalDataset from .word_sorting import TextTransformation, WordSortingConfig, WordSortingDataset -from .word_ladder import WordLadderConfig, WordLadderDataset __all__ = [ "SpellBackwardConfig", diff --git a/reasoning_gym/algorithmic/base_conversion.py b/reasoning_gym/algorithmic/base_conversion.py index eb0978bd..afa6200a 100644 --- a/reasoning_gym/algorithmic/base_conversion.py +++ b/reasoning_gym/algorithmic/base_conversion.py @@ -60,14 +60,32 @@ def __getitem__(self, idx: int) -> dict: value, source_base, target_base = self._generate_conversion(rng) # Convert decimal to source base representation - source_repr = format(value, f"x" if source_base == 16 else f"b" if source_base == 2 else "").strip() - if source_base not in (2, 16): - source_repr = format(value, f"{source_base}x").lower().strip() + if source_base == 16: + source_repr = format(value, "x") + elif source_base == 2: + source_repr = format(value, "b") + else: + # Manual conversion for other bases + n = value + digits = [] + while n: + digits.append(int(n % source_base)) + n //= source_base + source_repr = "".join(str(d) if d < 10 else chr(ord("a") + d - 10) for d in reversed(digits) or [0]) # Convert decimal to target base for answer - target_repr = format(value, f"x" if target_base == 16 else f"b" if target_base == 2 else "").strip() - if target_base not in (2, 16): - target_repr = format(value, f"{target_base}x").lower().strip() + if target_base == 16: + target_repr = format(value, "x") + elif target_base == 2: + target_repr = format(value, "b") + else: + # Manual conversion for other bases + n = value + digits = [] + while n: + digits.append(int(n % target_base)) + n //= target_base + target_repr = "".join(str(d) if d < 10 else chr(ord("a") + d - 10) for d in reversed(digits) or [0]) source_name = self._format_base_name(source_base) target_name = self._format_base_name(target_base) diff --git a/reasoning_gym/algorithmic/letter_counting.py b/reasoning_gym/algorithmic/letter_counting.py index 1ef33148..8f2590dd 100644 --- a/reasoning_gym/algorithmic/letter_counting.py +++ b/reasoning_gym/algorithmic/letter_counting.py @@ -51,7 +51,7 @@ def __getitem__(self, idx: int) -> dict: letters = {"a"} # Fallback if span has no letters # Select random letter that appears in the span - target_letter = rng.choice(list(letters)) + target_letter = rng.choice(sorted(letters)) # Count occurrences count = sum(word.lower().count(target_letter) for word in span) diff --git a/reasoning_gym/algorithmic/word_ladder.py b/reasoning_gym/algorithmic/word_ladder.py index d0342cf7..9a55e24e 100644 --- a/reasoning_gym/algorithmic/word_ladder.py +++ b/reasoning_gym/algorithmic/word_ladder.py @@ -1,13 +1,15 @@ """Word ladder task generator""" +from collections import deque from dataclasses import dataclass from random import Random -from typing import List, Optional, Set, Dict, Tuple -from collections import deque +from typing import Dict, List, Optional, Set, Tuple + from reasoning_gym.data import read_data_file from ..factory import ProceduralDataset, register_dataset + @dataclass class WordLadderConfig: """Configuration for word ladder task generation""" @@ -32,7 +34,9 @@ def validate(self) -> None: # Modified validation logic if self.min_chain_length == -1: if self.max_chain_length != -1: - assert self.max_chain_length >= 3, "When min_chain_length=-1 (shortest path), max_chain_length must be -1 or >=3" + assert ( + self.max_chain_length >= 3 + ), "When min_chain_length=-1 (shortest path), max_chain_length must be -1 or >=3" elif self.max_chain_length == -1: raise AssertionError("max_chain_length cannot be -1 unless min_chain_length is also -1") else: @@ -81,16 +85,17 @@ def _load_words_from_csv(cls, min_length: int = 3, max_length: int = 5) -> Dict[ import csv from io import StringIO + word_sets = {} - + try: # Get CSV content as string csv_content = read_data_file("words.csv") - + # Use StringIO to create a file-like object from the string csv_file = StringIO(csv_content) reader = csv.DictReader(csv_file) - + for row in reader: # Process each word length column using config range for length in range(min_length, max_length + 1): @@ -104,12 +109,12 @@ def _load_words_from_csv(cls, min_length: int = 3, max_length: int = 5) -> Dict[ except Exception as e: raise RuntimeError(f"Error processing words.csv content: {e}") from e - + # Validate we have words for each length for length in range(min_length, max_length + 1): if length not in word_sets or not word_sets[length]: raise ValueError(f"No valid words found for length {length}") - + return word_sets def _get_neighbors(self, word: str, word_set: Set[str]) -> Set[str]: @@ -160,7 +165,7 @@ def _find_path(self, start: str, end: str, word_set: Set[str]) -> Optional[List[ # Use basic BFS for shortest path queue = deque([(start, [start])]) visited = {start} - + while queue: current, path = queue.popleft() if current == end: @@ -212,12 +217,7 @@ def __getitem__(self, idx: int) -> dict: return { "question": f"Transform the word ladder '{start}' to '{end}' by changing one letter at a time.", "answer": ",".join(path), - "metadata": { - "start_word": start, - "end_word": end, - "word_length": length, - "chain_length": len(path) - } + "metadata": {"start_word": start, "end_word": end, "word_length": length, "chain_length": len(path)}, } -register_dataset("word_ladder", WordLadderDataset, WordLadderConfig) \ No newline at end of file +register_dataset("word_ladder", WordLadderDataset, WordLadderConfig) diff --git a/reasoning_gym/algorithmic/word_sorting.py b/reasoning_gym/algorithmic/word_sorting.py index b573fee1..8ac683b6 100644 --- a/reasoning_gym/algorithmic/word_sorting.py +++ b/reasoning_gym/algorithmic/word_sorting.py @@ -49,7 +49,7 @@ def __init__(self, config: WordSortingConfig): # Load and preprocess text text = read_data_file("in_the_year_2889.txt") # Extract unique words within length constraints - self.words = list( + self.words = sorted( set( word for word in re.findall(r"\b\w+\b", text) diff --git a/reasoning_gym/arithmetic/__init__.py b/reasoning_gym/arithmetic/__init__.py index 12a6ee89..9e1a5bc2 100644 --- a/reasoning_gym/arithmetic/__init__.py +++ b/reasoning_gym/arithmetic/__init__.py @@ -4,15 +4,18 @@ - Chain sums - Word problems - Leg counting +- Time intervals """ from .basic_arithmetic import BasicArithmeticDataset, BasicArithmeticDatasetConfig +from .calendar_arithmetic import CalendarArithmeticConfig, CalendarArithmeticDataset from .chain_sum import ChainSum, ChainSumConfig from .fraction_simplification import FractionSimplificationConfig, FractionSimplificationDataset from .gcd import GCDConfig, GCDDataset from .lcm import LCMConfig, LCMDataset from .leg_counting import LegCountingConfig, LegCountingDataset from .prime_factorization import PrimeFactorizationConfig, PrimeFactorizationDataset +from .time_intervals import TimeIntervalsConfig, TimeIntervalsDataset __all__ = [ "BasicArithmeticDataset", @@ -20,6 +23,10 @@ "basic_arithmetic_dataset", "ChainSum", "ChainSumConfig", + "CalendarArithmeticConfig", + "CalendarArithmeticDataset", + "Weekday", + "CalendarTask", "FractionSimplificationConfig", "FractionSimplificationDataset", "GCDConfig", @@ -30,4 +37,6 @@ "LegCountingDataset", "PrimeFactorizationConfig", "PrimeFactorizationDataset", + "TimeIntervalsConfig", + "TimeIntervalsDataset", ] diff --git a/reasoning_gym/arithmetic/calendar_arithmetic.py b/reasoning_gym/arithmetic/calendar_arithmetic.py new file mode 100644 index 00000000..78c42df8 --- /dev/null +++ b/reasoning_gym/arithmetic/calendar_arithmetic.py @@ -0,0 +1,490 @@ +import calendar +import math +import random +from dataclasses import dataclass +from datetime import date, timedelta +from enum import Enum, auto +from typing import Any, Dict, List, Optional, Tuple + +from ..factory import ProceduralDataset, register_dataset + + +class Weekday(Enum): + MONDAY = auto() + TUESDAY = auto() + WEDNESDAY = auto() + THURSDAY = auto() + FRIDAY = auto() + SATURDAY = auto() + SUNDAY = auto() + + @classmethod + def from_date(cls, d: date) -> "Weekday": + return list(cls)[d.weekday()] + + @classmethod + def random(cls, rng: random.Random) -> "Weekday": + return list(cls)[rng.randint(0, 6)] + + @classmethod + def __getitem__(cls, idx) -> "Weekday": + return list(cls)[idx] + + @property + def index(self) -> int: + return self.value - 1 + + def __str__(self) -> str: + return self.name.capitalize() + + +class CalendarTask(Enum): + WEEKDAY_OFFSET = "weekday_offset" + WEEKDAY_OF_DATE = "weekday_of_date" + WEEKDAY_OF_DATE_FROM_FIRST_DATE = "weekday_of_date_from_first_day" + RECURRING_EVENT_CALCULATIONS = "recurring_event_day" + COUNT_DAYS = "count_days" + COUNT_BUSINESS_DAYS = "count_business_days" + IS_LEAP_YEAR = "is_leap_year" + + +@dataclass +class CalendarArithmeticConfig: + year: int = 2022 + tasks: Optional[List[str]] = None + offset_upper_bound: int = 100 + leap_year_range: int = 200 + seed: Optional[int] = 42 + size: int = 500 + + def __post_init__(self): + if self.tasks is None: + self.tasks = [task.value for task in CalendarTask] + else: + self.tasks = [task.lower() for task in self.tasks] + valid_tasks = {task.value for task in CalendarTask} + invalid_tasks = set(self.tasks) - valid_tasks + if invalid_tasks: + valid_task_list = ", ".join(sorted(valid_tasks)) + raise ValueError( + f"Invalid tasks: {', '.join(sorted(invalid_tasks))}. " f"Valid tasks are: {valid_task_list}" + ) + + def validate(self) -> None: + """Validate the configuration parameters.""" + if not isinstance(self.year, int) or self.year <= 0: + raise ValueError(f"year must be a positive integer, got {self.year}") + + if self.seed is not None and not isinstance(self.seed, int): + raise ValueError(f"seed must be an integer or None, got {type(self.seed)}") + + if not isinstance(self.size, int) or self.size <= 0: + raise ValueError(f"size must be a positive integer, got {self.size}") + + +class CalendarArithmeticDataset(ProceduralDataset): + DAY_QUESTION_TEMPLATES = [ + "Answer with the weekday's name (e.g., Monday, Tuesday, etc.).", + "Provide the full name of the weekday.", + "State the weekday (Monday through Sunday).", + "Give the weekday name in full.", + "Reply with just the weekday name.", + "Write out the full weekday name.", + "Respond with the weekday (Monday-Sunday).", + "Answer using the complete weekday name.", + "Name the day of the week in full.", + ] + + COUNT_QUESTION_TEMPLATES = [ + "Answer with a number.", + "Provide the count as a number.", + "Respond with just the number.", + "Write the total number.", + "Give the count numerically.", + "State the amount as a number.", + "Reply with the numerical value.", + "Express your answer as a number.", + ] + + def __init__(self, config: CalendarArithmeticConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + + self.task_handlers = { + CalendarTask.WEEKDAY_OFFSET.value: self._weekday_offset, + CalendarTask.WEEKDAY_OF_DATE.value: self._weekday_of_date, + CalendarTask.WEEKDAY_OF_DATE_FROM_FIRST_DATE.value: self._weekday_of_date_from_first_day, + CalendarTask.RECURRING_EVENT_CALCULATIONS.value: self._recurring_event_day, + CalendarTask.COUNT_DAYS.value: self._count_days, + CalendarTask.COUNT_BUSINESS_DAYS.value: self._count_business_days, + CalendarTask.IS_LEAP_YEAR.value: self._is_leap_year, + } + + self.tasks = [self.task_handlers[task] for task in self.config.tasks] + + def __getitem__(self, idx: int) -> dict: + item_rng = random.Random(self.seed + idx) + task = item_rng.choice(self.tasks) + question, answer, metadata = task(item_rng) + return { + "question": question, + "answer": str(answer), + "metadata": metadata, + } + + def _weekday_offset(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + Task: Given a starting date and a day offset (which may be positive or negative), + ask what day of the week it will be. + Examples: + - "If today is Wednesday, March 13, 2024, what day of the week will it be in 10 days? Answer with the weekday's name." + - "If today is Wednesday, March 13, 2024, what day of the week was it 10 days ago? Answer with the weekday's name." + """ + year = self.config.year + start_date = self._random_date_for_year(rng, year) + offset = rng.randint(1, self.config.offset_upper_bound) + sign = rng.choice([-1, 1]) + offset_days = sign * offset + target_date = start_date + timedelta(days=offset_days) + target_weekday = target_date.strftime("%A") + + date_str = f"{start_date.strftime('%A')}, {start_date.strftime('%B')} {start_date.day}, {start_date.year}" + if offset_days >= 0: + templates = [ + f"If today is {date_str}, what day of the week will it be in {offset_days} days? ", + f"Starting from {date_str}, which weekday falls after a {offset_days}-day jump? ", + f"Count forward {offset_days} days from {date_str} - what's the weekday? ", + ] + else: + templates = [ + f"If today is {date_str}, what day of the week was it {abs(offset_days)} days ago? ", + f"Starting from {date_str}, which weekday was it {abs(offset_days)} days before? ", + f"Count backward {abs(offset_days)} days from {date_str} - what's the weekday? ", + ] + + question = rng.choice(templates) + rng.choice(self.DAY_QUESTION_TEMPLATES) + metadata = { + "task": CalendarTask.WEEKDAY_OFFSET.value, + "start_date": start_date.isoformat(), + "offset_days": offset_days, + "target_date": target_date.isoformat(), + } + return question, target_weekday, metadata + + def _weekday_of_date(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: Ask what day of the week a given date was. + example: + "What day of the week was January 15, 2024? + Answer with the weekday's name." + """ + year = self.config.year + target_date = self._random_date_for_year(rng, year) + answer_weekday = target_date.strftime("%A") + templates = [ + f"What day of the week was {target_date.strftime('%B')} {target_date.day}, {year}?", + f"On which weekday did {target_date.strftime('%B')} {target_date.day}, {year} fall?", + f"Name the day of the week for {target_date.strftime('%m/%d/%Y')}.", + ] + + question = f"{rng.choice(templates)} {rng.choice(self.DAY_QUESTION_TEMPLATES)}" + metadata = { + "task": CalendarTask.WEEKDAY_OF_DATE.value, + "target_date": target_date.isoformat(), + } + return question, answer_weekday, metadata + + def _weekday_of_date_from_first_day(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: Given an hypothetical weekday for January 1, ask what weekday a later date in the year falls on. + example: + "If the first day of the year was a Monday, what day of the week will December 31 be? + Answer with the weekday's name." + """ + year = self.config.year + first_day = Weekday.random(rng) + first_day_index = first_day.index + # Ensure target date is not January 1. + year_start = date(year, 1, 1) + year_end = date(year, 12, 31) + max_delta = timedelta(days=self.config.offset_upper_bound) + max_date = min(year_start + max_delta, year_end) + while True: + target_date = self._random_date_between(rng, year_start, max_date) + if target_date != date(year, 1, 1): + break + delta_days = (target_date - date(year, 1, 1)).days + answer_index = (first_day_index + delta_days) % 7 + answer_weekday = Weekday(answer_index + 1) + + templates = [ + f"If the first day of the year was a {first_day}, what day of the week will " + f"{target_date.strftime('%B')} {target_date.day} be? ", + f"Given that January 1 fell on a {first_day}, which weekday occurs on " + f"{target_date.strftime('%B')} {target_date.day}? ", + f"In a year where {first_day} is January 1st, name the weekday of " + f"{target_date.strftime('%B')} {target_date.day}. ", + ] + + question = rng.choice(templates) + rng.choice(self.DAY_QUESTION_TEMPLATES) + metadata = { + "task": CalendarTask.WEEKDAY_OF_DATE_FROM_FIRST_DATE.value, + "year": year, + "first_day": str(first_day), + "target_date": target_date.isoformat(), + "delta_days": delta_days, + } + return question, answer_weekday, metadata + + def _recurring_event_day(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: For a recurring event defined by an ordinal weekday pattern in a month, + ask on which day of the month the event occurs. + example: + "If a meeting is scheduled on the second Tuesday of May 2024, on which day does it fall? + Answer with a number." + """ + year = self.config.year + month = rng.randint(1, 12) + ordinals = ["first", "second", "third", "fourth", "last"] + ordinal = rng.choice(ordinals) + weekday = Weekday.random(rng) + month_name = calendar.month_name[month] + _, last_day = calendar.monthrange(year, month) + + if ordinal != "last": + ordinal_number = {"first": 1, "second": 2, "third": 3, "fourth": 4}[ordinal] + count = 0 + event_day = None + for day in range(1, last_day + 1): + d = date(year, month, day) + if d.strftime("%A") == str(weekday): + count += 1 + if count == ordinal_number: + event_day = day + break + if event_day is None: + # This should rarely happen but in some months the ordinal may not exist. + event_day = -1 + else: + event_day = None + for day in range(last_day, 0, -1): + d = date(year, month, day) + if d.strftime("%A") == str(weekday): + event_day = day + break + if event_day is None: + event_day = -1 + + templates = [ + f"If a meeting is scheduled on the {ordinal} {weekday} of {month_name} {year}, on which day of the month does it occur? ", + f"In {month_name} {year}, if an event recurs on the {ordinal} {weekday}, what is the date (day of the month) of the event? ", + f"Determine the day of the month for the {ordinal} {weekday} in {month_name} {year}. ", + ] + question = ( + rng.choice(templates) + + rng.choice(self.COUNT_QUESTION_TEMPLATES) + + " Answer with -1 if the ordinal does not exist in the month." + ) + metadata = { + "task": CalendarTask.RECURRING_EVENT_CALCULATIONS.value, + "year": year, + "month": month, + "ordinal": ordinal, + "weekday": str(weekday), + } + return question, str(event_day), metadata + + def _count_days(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: Ask how many times a given weekday occurs in a specified range. + example: + "How many days are there between March 1, 2024 and March 15, 2024? + Answer with a number." + """ + year = self.config.year + year_start = date(year, 1, 1) + year_end = date(year, 12, 31) + start_date = self._random_date_between(rng, year_start, year_end) + max_delta = timedelta(days=self.config.offset_upper_bound) + end_date = self._random_date_between(rng, start_date, min(year_end, start_date + max_delta)) + weekday = Weekday.random(rng) + + def count_weekday_between(d1: date, d2: date, weekday: str) -> int: + days = (d2 - d1).days + 1 + return sum(1 for i in range(days) if (d1 + timedelta(days=i)).strftime("%A") == weekday) + + count = count_weekday_between(start_date, end_date, str(weekday)) + + templates = [ + f"How many {weekday}s are there from {start_date.strftime('%A, %B')} {start_date.day}, {year} to " + f"{end_date.strftime('%A, %B')} {end_date.day}, {year} (inclusive of both dates)? ", + f"Count the occurrences of {weekday} from {start_date.strftime('%A, %B')} {start_date.day} " + f"to {end_date.strftime('%A, %B')} {end_date.day}, {year} (including both start and end dates). ", + f"Between {start_date.strftime('%A, %B')} {start_date.day}, {year} and " + f"{end_date.strftime('%A, %B')} {end_date.day}, {year} " + f"(counting both dates), how many times does {weekday} occur? ", + ] + + question = rng.choice(templates) + rng.choice(self.COUNT_QUESTION_TEMPLATES) + metadata = { + "task": CalendarTask.COUNT_DAYS.value, + "year": year, + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + } + return question, str(count), metadata + + def _count_business_days(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: Count the number of business days (Monday-Friday) between two dates. + example: + "How many business days (Monday-Friday) are there between March 1, 2024 and March 15, 2024? + Answer with a number." + """ + year = self.config.year + year_start = date(year, 1, 1) + year_end = date(year, 12, 31) + start_date = self._random_date_between(rng, year_start, year_end) + max_delta = timedelta(days=self.config.offset_upper_bound) + end_date = self._random_date_between(rng, start_date, start_date + max_delta) + + count = 0 + + def business_days_between(d1: date, d2: date) -> int: + days = (d2 - d1).days + 1 + weeks, remainder = divmod(days, 7) + count = weeks * 5 + start_weekday = d1.weekday() + for i in range(remainder): + if (start_weekday + i) % 7 < 5: + count += 1 + return count + + count = business_days_between(start_date, end_date) + + templates = [ + f"How many business days (Monday-Friday) are there from " + f"{start_date.strftime('%A, %B')} {start_date.day}, {year} to " + f"{end_date.strftime('%A, %B')} {end_date.day}, {year} " + f"(inclusive of both dates)? ", + f"Count the weekdays (excluding weekends) from " + f"{start_date.strftime('%A, %B')} {start_date.day} to " + f"{end_date.strftime('%A, %B')} {end_date.day}, {year} " + f"(including both start and end dates). ", + f"Between {start_date.strftime('%A, %B')} {start_date.day}, {year} and " + f"{end_date.strftime('%A, %B')} {end_date.day}, {year} " + f"(counting both dates), what's the total count of business days " + f"(Monday through Friday)? ", + ] + + question = rng.choice(templates) + rng.choice(self.COUNT_QUESTION_TEMPLATES) + metadata = { + "task": CalendarTask.COUNT_BUSINESS_DAYS.value, + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + } + return question, str(count), metadata + + def _is_leap_year(self, rng: random.Random) -> Tuple[str, str, dict]: + """ + task: Given a year, determine whether it is a leap year. + example: + "Is 2024 a leap year? Answer with Yes or No." + """ + semirange = self.config.leap_year_range // 2 + year = rng.randint(self.config.year - semirange, self.config.year + semirange) + is_leap = calendar.isleap(year) + answer = "Yes" if is_leap else "No" + templates = [ + f"Determine if the year {year} is a leap year. ", + f"Is {year} a leap year? ", + f"Tell me whether {year} is a leap year. ", + ] + question = rng.choice(templates) + "Answer with Yes or No." + metadata = { + "task": CalendarTask.IS_LEAP_YEAR.value, + "year": year, + "is_leap": is_leap, + } + return question, answer, metadata + + def _random_date_for_year(self, rng: random.Random, year: int) -> date: + """Return a random date within the given year.""" + month = rng.randint(1, 12) + _, last_day = calendar.monthrange(year, month) + day = rng.randint(1, last_day) + return date(year, month, day) + + def _random_date_between(self, rng: random.Random, start_date: date, end_date: date) -> date: + """ + Return a random date between start_date and end_date (inclusive). + Assumes start_date <= end_date. + """ + if start_date > end_date: + raise ValueError("start_date must be <= end_date") + delta = (end_date - start_date).days + random_days = rng.randint(0, delta) + return start_date + timedelta(days=random_days) + + def score_answer(self, answer: Optional[str], entry: Dict[str, Any]) -> float: + # we suppose the answer is the last occurence of the expected answer type + if answer is None: + return 0.0 + + oracle_answer = entry["answer"] + task = entry["metadata"]["task"] + + if task in { + CalendarTask.WEEKDAY_OFFSET.value, + CalendarTask.WEEKDAY_OF_DATE_FROM_FIRST_DATE.value, + CalendarTask.WEEKDAY_OF_DATE.value, + }: + if not answer: + return 0.0 + + answer = answer.strip() + oracle_answer = oracle_answer + weekdays = {d.name.title() for d in Weekday} + + if answer == oracle_answer: + return 1.0 + + if answer in weekdays: + return 0.1 + + if answer.title() in weekdays: + return 0.05 + + if answer.title() not in weekdays: + return 0.0 + + return 0.0 + + # denser reward for numerical tasks + elif task in { + CalendarTask.COUNT_BUSINESS_DAYS.value, + CalendarTask.COUNT_DAYS.value, + CalendarTask.RECURRING_EVENT_CALCULATIONS.value, + }: + try: + ans_num = int(answer.strip()) + oracle_num = int(oracle_answer.strip()) + + if oracle_num == 0: + return 1.0 if ans_num == 0 else 0.0 + + relative_error = abs(ans_num - oracle_num) / oracle_num + return max(0.0, math.exp(-5 * relative_error)) + + except (ValueError, AttributeError): + return 0.0 + + elif task == CalendarTask.IS_LEAP_YEAR.value: + if answer.strip().lower() == oracle_answer.lower(): + return 1.0 + return 0.0 + + return 0.0 + + +register_dataset("calendar_arithmetic", CalendarArithmeticDataset, CalendarArithmeticConfig) diff --git a/reasoning_gym/arithmetic/time_intervals.py b/reasoning_gym/arithmetic/time_intervals.py new file mode 100644 index 00000000..1b296d02 --- /dev/null +++ b/reasoning_gym/arithmetic/time_intervals.py @@ -0,0 +1,323 @@ +import random +from dataclasses import dataclass, field +from datetime import date, datetime, time, timedelta +from typing import List, Optional + +import pytz +from dateutil import parser + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class TimeIntervalsConfig: + """Configuration for time interval calculation tasks""" + + min_time: time = time.min + max_time: time = time.max + max_time_difference_seconds: int = 24 * 60 * 60 + min_date: date = date(1900, 1, 1) + max_date: date = date(3000, 1, 1) + max_date_difference_days: int = 100 + task_types: List[str] = field( + default_factory=lambda: ["time", "time_seconds", "time_ms", "date", "datetime", "datetime_tz"] + ) + seed: Optional[int] = None + size: int = 500 + + def validate(self) -> None: + """Validate configuration parameters""" + assert self.size > 0, "size must be positive" + assert self.max_time_difference_seconds > 0, "max_time_difference_seconds must be positive" + assert self.max_date_difference_days > 0, "max_date_difference_days must be positive" + assert self.min_date < self.max_date, "min_date must be before max_date" + + +class TimeIntervalsDataset(ProceduralDataset): + """Generates time interval calculation tasks with various formats and complexities""" + + TEMPLATES = [ + "What is the duration between {start} and {end}? Please answer in {format}.", + "Calculate the time difference between {start} and {end}. Express the result in {format}.", + "How much time elapsed from {start} to {end}? Give your answer in {format}.", + "A meeting started at {start} and ended at {end}. How long was the meeting? Answer in {format}.", + "A system operation started at {start} and completed at {end}. What was the operation duration? Answer in {format}.", + "A database query started at {start} and ended at {end}. How long did the query take? Answer in {format}.", + "A flight departed at {start} and arrived at {end}. How long was the flight? Answer in {format}.", + "A video call started at {start} and ended at {end}. How long was the call? Answer in {format}.", + "A system backup started at {start} and completed at {end}. What was the total backup duration? Answer in {format}.", + "A conference call began at {start} and ended at {end}. How long was the conference? Answer in {format}.", + ] + + TIME_FORMATS = [ + "%H:%M", + "%H:%M:%S", + "%H:%M:%S.%f", + ] + + DATE_FORMATS = [ + "%Y-%m-%d", + "%B %d, %Y", + "%m/%d/%Y", + "%A, %B %d, %Y", # e.g. Monday, January 15, 2024 + "%a %b %d %Y", # e.g. Mon Jan 15 2024 + "%d %B %Y", # e.g. 15 January 2024 + "%Y-%m-%d (%A)", # e.g. 2024-01-15 (Monday) + ] + + DATETIME_FORMATS = [ + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M %z", # For UTC offset format + "%Y-%m-%d %H:%M:%S %z", # For UTC offset with seconds + "%A, %B %d, %Y at %H:%M", # e.g. Monday, January 15, 2024 at 14:30 + "%a %b %d %Y %H:%M:%S", # e.g. Mon Jan 15 2024 14:30:45 + "%d %B %Y, %H:%M", # e.g. 15 January 2024, 14:30 + "%d %B %Y, %H:%M %z", # e.g. 15 January 2024, 14:30 +0000 + "%Y-%m-%d (%A) %H:%M:%S %z", # e.g. 2024-01-15 (Monday) 14:30:45 +0000 + ] + + def __init__(self, config: TimeIntervalsConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """Generate a single time interval calculation task""" + item_rng = random.Random(self.seed + idx) + + # Randomly choose task type from config + task_type = item_rng.choice(self.config.task_types) + + start_time, end_time, format_str, expected_format = self._generate_times(item_rng, task_type) + + template = item_rng.choice(self.TEMPLATES) + question = template.format(start=start_time, end=end_time, format=expected_format) + + # Calculate the actual difference + if isinstance(start_time, str): + # Handle datetime strings with weekday names in parentheses + start_time = start_time.split(" (")[0] # Remove (Weekday) if present + end_time = end_time.split(" (")[0] + # Parse with UTC offset handling + start_dt = parser.parse(start_time) + end_dt = parser.parse(end_time) + else: + start_dt = start_time + end_dt = end_time + + difference = end_dt - start_dt + + # Format the answer according to expected_format + if expected_format == "HH:MM": + total_seconds = difference.total_seconds() + answer = f"{int(total_seconds // 3600):02d}:{int((total_seconds % 3600) // 60):02d}" + elif expected_format == "HH:MM:SS": + total_seconds = difference.total_seconds() + answer = f"{int(total_seconds // 3600):02d}:{int((total_seconds % 3600) // 60):02d}:{int(total_seconds % 60):02d}" + elif expected_format == "HH:MM:SS.mmm": + total_seconds = difference.total_seconds() + ms = int((total_seconds % 1) * 1000) + answer = f"{int(total_seconds // 3600):02d}:{int((total_seconds % 3600) // 60):02d}:{int(total_seconds % 60):02d}.{ms:03d}" + elif expected_format == "D days": + answer = f"{difference.days} days" + else: # "D days, HH:MM" or "D days, HH:MM:SS" + days = difference.days + hours = difference.seconds // 3600 + minutes = (difference.seconds % 3600) // 60 + seconds = difference.seconds % 60 + if expected_format == "D days, HH:MM:SS": + answer = f"{days} days, {hours:02d}:{minutes:02d}:{seconds:02d}" + else: # "D days, HH:MM" + answer = f"{days} days, {hours:02d}:{minutes:02d}" + + return { + "question": question, + "answer": answer, + "metadata": { + "task_type": task_type, + "start_time": start_dt, + "end_time": end_dt, + "format": format_str, + "expected_format": expected_format, + }, + } + + def _generate_times(self, rng: random.Random, task_type: str): + """Generate start and end times based on task type""" + if task_type.startswith("time"): + if task_type == "time_ms": + format_str = self.TIME_FORMATS[2] # Get milliseconds format + expected_format = "HH:MM:SS.mmm" + else: + format_str = next(f for f in self.TIME_FORMATS if f.count(":") == (2 if "seconds" in task_type else 1)) + expected_format = "HH:MM:SS" if "seconds" in task_type else "HH:MM" + + # Generate random start time + start_hour = rng.randint(0, 23) + start_minute = rng.randint(0, 59) + start_second = rng.randint(0, 59) + base = datetime.combine(date.today(), time(start_hour, start_minute, start_second)) + + # Calculate seconds remaining until midnight + seconds_until_midnight = ((24 - start_hour) * 3600) - (start_minute * 60) - start_second + # Use the minimum of config max and seconds until midnight + max_seconds = min(self.config.max_time_difference_seconds, seconds_until_midnight) + diff_seconds = rng.randint(1, max_seconds) if max_seconds > 0 else 0 + + if task_type == "time_ms": + # Add microseconds for millisecond precision + base = base.replace(microsecond=rng.randint(0, 999) * 1000) + end_time = base + timedelta(seconds=diff_seconds, microseconds=rng.randint(0, 999) * 1000) + # Format with exactly 3 decimal places for milliseconds + start_time = base.strftime(format_str)[:-3] # Remove extra microsecond digits + end_time = end_time.strftime(format_str)[:-3] # Remove extra microsecond digits + else: + start_time = base.strftime(format_str) + end_time = (base + timedelta(seconds=diff_seconds)).strftime(format_str) + + elif task_type == "date": + format_str = rng.choice(self.DATE_FORMATS) + expected_format = "D days" # Always return number of days for date tasks + + # Generate random start date within configured range, leaving room for end date + max_date_difference_days = min( + self.config.max_date_difference_days, (self.config.max_date - self.config.min_date).days + ) + max_start_days = (self.config.max_date - self.config.min_date).days - max_date_difference_days + start_days = rng.randint(0, max_start_days - 1) + start_date = self.config.min_date + timedelta(days=start_days) + + # Ensure positive difference between dates + diff_days = rng.randint(0, max_date_difference_days) + end_date = start_date + timedelta(days=diff_days) + + start_time = start_date.strftime(format_str) + end_time = end_date.strftime(format_str) + + else: # datetime or datetime_tz + format_str = rng.choice(self.DATETIME_FORMATS) + # Choose between HH:MM and HH:MM:SS format for datetime answers + expected_format = rng.choice(["D days, HH:MM", "D days, HH:MM:SS"]) + + # Generate random start datetime + days_range = (self.config.max_date - self.config.min_date).days + start_days = rng.randint(0, days_range) + start_hour = rng.randint(0, 23) + start_minute = rng.randint(0, 59) + start_second = rng.randint(0, 59) + + # Generate random time differences first + diff_days = rng.randint(0, self.config.max_date_difference_days) + diff_seconds = rng.randint(1, self.config.max_time_difference_seconds) + + if "%z" in format_str: + # Use simpler timezone format with offset + base = datetime.combine( + self.config.min_date + timedelta(days=start_days), time(start_hour, start_minute, start_second) + ) + # Generate timezone offsets + start_offset = rng.randint(-12, 12) + end_offset = rng.randint(-12, 12) + + # Apply start timezone + base = base.replace(tzinfo=pytz.FixedOffset(start_offset * 60)) + start_format = format_str.replace("%z", "%+05d" % (start_offset * 100)) + + # Calculate end time and convert to end timezone + end_dt = base + timedelta(days=diff_days, seconds=diff_seconds) + end_dt = end_dt.replace(tzinfo=pytz.FixedOffset(end_offset * 60)) + end_format = format_str.replace("%z", "%+05d" % (end_offset * 100)) + + # Format times with their respective timezone offsets + start_time = base.strftime(start_format).rstrip() + end_time = end_dt.strftime(end_format).rstrip() + else: + base = datetime.combine( + self.config.min_date + timedelta(days=start_days), time(start_hour, start_minute, start_second) + ) + # For non-timezone aware times, both use same format + start_time = base.strftime(format_str).rstrip() + end_time = (base + timedelta(days=diff_days, seconds=diff_seconds)).strftime(format_str).rstrip() + + return start_time, end_time, format_str, expected_format + + def score_answer(self, answer: Optional[str], entry: dict) -> float: + """Score an answer based on how close it is to the expected duration + + Returns a score between 0 and 1, with partial credit for answers that are + close to correct in the appropriate units/format + """ + if not answer: + return 0.0 + + expected = entry["answer"] + task_type = entry["metadata"]["task_type"] + + try: + if task_type == "date": + # Parse "X days" format + try: + actual = int(answer.strip().split()[0]) # Get number before "days" + expected = int(expected.strip().split()[0]) + if actual == expected: + return 1.0 + # Partial credit based on how close the day count is + max_diff = self.config.max_date_difference_days + diff = abs(actual - expected) + return max(0.0, 1.0 - (diff / max_diff)) + except (ValueError, IndexError): + return 0.0 + + elif task_type.startswith("time"): + # Parse times into total seconds for comparison + def parse_time(t): + parts = t.strip().split(":") + seconds = int(parts[0]) * 3600 + int(parts[1]) * 60 + if len(parts) > 2: + if "." in parts[2]: # Has milliseconds + s, ms = parts[2].split(".") + seconds += int(s) + int(ms) / 1000 + else: + seconds += int(parts[2]) + return seconds + + actual_seconds = parse_time(answer) + expected_seconds = parse_time(expected) + + if actual_seconds == expected_seconds: + return 1.0 + + # Partial credit based on how close the times are + max_diff = self.config.max_time_difference_seconds + diff = abs(actual_seconds - expected_seconds) + return max(0.0, 1.0 - (diff / max_diff)) + + else: # datetime or datetime_tz + # Parse the complex format "X days, HH:MM" or "X days, HH:MM:SS" + def parse_datetime(t): + days = int(t.split(" days,")[0]) + time_part = t.split(",")[1].strip() + parts = time_part.split(":") + seconds = int(parts[0]) * 3600 + int(parts[1]) * 60 + if len(parts) > 2: + seconds += int(parts[2]) + return days * 86400 + seconds + + actual_seconds = parse_datetime(answer) + expected_seconds = parse_datetime(expected) + + if actual_seconds == expected_seconds: + return 1.0 + + # Partial credit based on total time difference + max_diff = self.config.max_date_difference_days * 86400 + diff = abs(actual_seconds - expected_seconds) + return max(0.0, 1.0 - (diff / max_diff)) + + except (ValueError, IndexError): + return 0.0 # Invalid format + + return 0.0 + + +# Register the dataset +register_dataset("time_intervals", TimeIntervalsDataset, TimeIntervalsConfig) diff --git a/reasoning_gym/code/__init__.py b/reasoning_gym/code/__init__.py new file mode 100644 index 00000000..d250ad6e --- /dev/null +++ b/reasoning_gym/code/__init__.py @@ -0,0 +1,10 @@ +""" +Cognition tasks for training reasoning capabilities: +- Code Analysis +- Code Interpretation +- Code Execution +""" + +from .bf import BFConfig, BFDataset + +__all__ = ["BFConfig", "BFDataset"] diff --git a/reasoning_gym/code/bf.py b/reasoning_gym/code/bf.py new file mode 100644 index 00000000..c2697203 --- /dev/null +++ b/reasoning_gym/code/bf.py @@ -0,0 +1,132 @@ +from dataclasses import dataclass +from random import Random +from typing import Dict, Optional + +import bfi + +from ..data.wordle_words import wordle_words +from ..factory import ProceduralDataset, register_dataset +from .contrib.bfit.Compiler import Compiler, Minify + + +@dataclass +class BFConfig: + """Configuration for BF task generation""" + + seed: Optional[int] = None + size: int = 500 + difficulty: int = 1 + + def validate(self) -> None: + """Validate configuration parameters""" + assert self.difficulty > 0, "difficulty must be greater than 0" + assert self.difficulty < 4, "difficulty must be less than 4" + + +class BFDataset(ProceduralDataset): + """Generates BF tasks""" + + def __init__(self, config: BFConfig): + self._prompt_templates = [ + "This is a BF (Brainf*ck) computer program. What is the output? \n\n{bf_program}", + ] + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """Generate a single BF task + + Returns: + dict with keys: + - question: str, the task description with BF program + - answer: str, the result of this BF program BFI execution + - metadata: dict with generation parameters + """ + rng = Random(self.seed + idx) + + bfit_code = self.generate_bfit_code(self.config.difficulty, rng) + bf_program = self.compile_bfit_code_to_bf(bfit_code) + result = bfi.interpret(bf_program, buffer_output=True) + + return { + "question": rng.choice(self._prompt_templates).format(bf_program=bf_program), + "answer": result, + "metadata": {"bfit_code": bfit_code, "bf_program": bf_program}, + } + + def generate_bfit_code(self, difficulty, rng: Random) -> str: + + if difficulty == 1: + word = rng.choice(wordle_words) + bfit_template = f""" +int main() {{ + print("{word}"); +}} +""" + elif difficulty == 2: + x = rng.randint(1, 4) + y = rng.randint(1, 5) + target = x * y * rng.randint(1, 9) + rng.randint(1, 9) + bfit_template = f""" +int main() {{ + int acc = 0; + int target = {target}; + int x = {x}; + int y = {y}; + while (acc < target) {{ + acc = acc + x; + acc = acc + y; + }} + printint(acc); +}} +""" + elif difficulty == 3: + x = rng.randint(1, 7) + y = rng.randint(1, 9) + target = x * y * rng.randint(1, 9) + rng.randint(1, 9) + 50 + conditional = target - rng.randint(1, 40) + bfit_template = f""" +int main() {{ + int acc = 0; + int target = {target}; + int x = {x}; + int y = {y}; + while (acc < target) {{ + acc = acc + x; + if (acc > {conditional}) {{ + acc = acc + y; + }} + }} + printint(acc); +}} +""" + rendered_bfit = bfit_template + return rendered_bfit + + def compile_bfit_code_to_bf(self, bfit: str) -> str: + bf = Compiler.compile(bfit, optimize_code=True) + # bf = Minify.minify(bf) # Is this necessary? + return bf + + def score_answer(self, answer: Optional[str], entry: Dict[str, any]) -> float: + """Determine if the solution provided solves the BF task. + + The function awards 1.0 for a correct answer. + + Args: + answer (Optional[str]): The user's answer. + entry (Dict[str, any]): The original dataset entry containing the correct answer. + + Returns: + float: The computed score between 0.0 and 1.0. + """ + + if answer == None: + return 0.0 + if answer != entry["answer"]: + return 0.01 + else: + return 1.0 # Yay + + +# Register the dataset +register_dataset("bf", BFDataset, BFConfig) diff --git a/reasoning_gym/code/contrib/__init__.py b/reasoning_gym/code/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reasoning_gym/code/contrib/bfit/BF-it.py b/reasoning_gym/code/contrib/bfit/BF-it.py new file mode 100644 index 00000000..ccdb3fc0 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/BF-it.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import argparse +import os + +import Interpreter +from Compiler import Compiler, Minify + + +def process_args(): + parser = argparse.ArgumentParser() + parser.add_argument("filepath", metavar="input_file", nargs=1, help="Path to the input code file") + parser.add_argument("--output", "-o", metavar="output_file", nargs=1, help="Path to output Brainfuck file") + parser.add_argument("--run", "-r", action="store_true", help="Run the Brainfuck file after compilation") + parser.add_argument("--minify", "-m", action="store_true", help="Minifies the compiled code") + parser.add_argument("--optimize", "-opt", action="store_true", help="syntax optimization") + + args = parser.parse_args() + + input_file = args.filepath[0] + if args.output: + output_file = args.output[0] + else: + output_file_basename = os.path.splitext(os.path.basename(input_file))[0] + ".bf" + output_file = os.path.join(os.path.dirname(input_file), output_file_basename) + + run_file = args.run + minify_file = args.minify + optimize = args.optimize + + return input_file, output_file, run_file, minify_file, optimize + + +def compile_file(): + input_file, output_file, run_file, minify_bf_code, optimize_code = process_args() + print("Compiling file '%s'..." % input_file) + + with open(input_file, "rb") as f: + code = f.read().decode("utf8") + + brainfuck_code = Compiler.compile(code, optimize_code) + brainfuck_code += "\n" + + if minify_bf_code: + brainfuck_code = Minify.minify(brainfuck_code) + + with open(output_file, "wt") as f: + f.write(brainfuck_code) + + print("Compiled successfully to '%s'" % output_file) + + if run_file: + print("Running compiled code...") + Interpreter.brainfuck(brainfuck_code) + + +if __name__ == "__main__": + compile_file() diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Compiler.py b/reasoning_gym/code/contrib/bfit/Compiler/Compiler.py new file mode 100644 index 00000000..e1f60258 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Compiler.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +from .Exceptions import BFSemanticError, BFSyntaxError +from .FunctionCompiler import FunctionCompiler +from .Functions import check_function_exists, get_function_object, insert_function_object +from .General import get_literal_token_code, is_token_literal, unpack_literal_tokens_to_array_dimensions +from .Globals import ( + create_variable_from_definition, + get_global_variables_size, + get_variable_dimensions, + get_variable_size, + insert_global_variable, +) +from .Lexical_analyzer import analyze +from .LibraryFunctionCompiler import insert_library_functions +from .Optimizer import optimize +from .Parser import Parser +from .Token import Token + +""" +This file is responsible for creating FunctionCompiler objects and global variables objects +And finally, return the code of the main function +""" + + +class Compiler: + def __init__(self, code, optimize_code=False): + tokens = analyze(code) + if optimize_code: + optimize(tokens) + self.parser = Parser(tokens) + + # global variables and functions + def create_function_object(self): + # function: (INT | VOID) ID LPAREN expression_list RPAREN LBRACE statements RBRACE + # returns function named tuple + + if self.parser.current_token().type not in [Token.VOID, Token.INT]: + raise BFSemanticError( + "Function return type can be either void or int, not '%s'" % str(self.parser.current_token()) + ) + + self.parser.check_next_tokens_are([Token.ID, Token.LPAREN]) + + # save all tokens of this function + function_name = self.parser.next_token(next_amount=1).data + RPAREN_index = self.parser.find_matching( + starting_index=self.parser.current_token_index + 2 + ) # first find RPAREN + self.parser.check_next_token_is(Token.LBRACE, starting_index=RPAREN_index) + RBRACE_index = self.parser.find_matching(starting_index=RPAREN_index + 1) # then find RBRACE + + # take all tokens between INT and RBRACE and pass them to function object + function_tokens = self.parser.tokens[self.parser.current_token_index : RBRACE_index + 1] + # skip function definition + self.parser.advance_to_token_at_index(RBRACE_index + 1) + + function = FunctionCompiler(function_name, function_tokens) + return function + + def compile_global_variable_definition(self): + # INT ID (ASSIGN NUM | (LBRACK NUM RBRACK)+ (ASSIGN LBRACE ... RBRACE)?)? SEMICOLON + # returns code that initializes this variable, and advances pointer according to variable size + + self.parser.check_current_tokens_are([Token.INT, Token.ID]) + ID_token = self.parser.next_token() + variable = create_variable_from_definition(self.parser, advance_tokens=True) + insert_global_variable(variable) + + # if this is set to False, then the compiler assumes that initially all cells are set to zero + # if this is set to True, then the compiler zeros each cell before using it (may generate a lot of unnecessary BF code) + ZERO_CELLS_BEFORE_USE = False + + code = "[-]" if ZERO_CELLS_BEFORE_USE else "" + if get_variable_size(variable) > 1: # its an array + if self.parser.current_token().type == Token.SEMICOLON: + # array definition - INT ID (LBRACK NUM RBRACK)+ SEMICOLON + self.parser.advance_token() # skip SEMICOLON + code = (code + ">") * get_variable_size(variable) # advance to after this variable + return code + elif self.parser.current_token().type == Token.ASSIGN and self.parser.current_token().data == "=": + # array definition and initialization - INT ID (LBRACK NUM RBRACK)+ ASSIGN ((LBRACE ... RBRACE)+|STRING) SEMICOLON + self.parser.advance_token() # skip ASSIGN + + if self.parser.current_token().type not in [Token.LBRACE, Token.STRING]: + raise BFSyntaxError("Expected LBRACE or STRING at '%s'" % self.parser.current_token()) + + literal_tokens_list = self.parser.compile_array_initialization_list() + self.parser.check_current_token_is(Token.SEMICOLON) + self.parser.advance_token() # skip SEMICOLON + + array_dimensions = get_variable_dimensions(variable) + unpacked_literals_list = unpack_literal_tokens_to_array_dimensions( + ID_token, array_dimensions, literal_tokens_list + ) + + for literal in unpacked_literals_list: + code += get_literal_token_code(literal) # evaluate this literal and point to next array element + return code + else: + raise BFSyntaxError( + "Unexpected %s in array definition. Expected SEMICOLON (;) or ASSIGN (=)" + % self.parser.current_token() + ) + + elif self.parser.current_token().type == Token.SEMICOLON: # no need to initialize + self.parser.advance_token() # skip SEMICOLON + code += ">" # advance to after this variable + else: + self.parser.check_current_token_is(Token.ASSIGN) + if self.parser.current_token().data != "=": + raise BFSyntaxError( + "Unexpected %s when initializing global variable. Expected ASSIGN (=)" % self.parser.current_token() + ) + self.parser.advance_token() # skip ASSIGN + + if not is_token_literal(self.parser.current_token()): + raise BFSemanticError( + "Unexpected '%s'. expected literal (NUM | CHAR | TRUE | FALSE )" % str(self.parser.current_token()) + ) + + code += get_literal_token_code(self.parser.current_token()) + + self.parser.check_next_token_is(Token.SEMICOLON) + self.parser.advance_token(amount=2) # skip (NUM|CHAR|TRUE|FALSE) SEMICOLON + + return code + + def process_global_definitions(self): + """ + Iterate through all tokens + When encountering function definition - create Function object and pass it the function's tokens + When encountering global variable definition - create Variable object + Returns code that initializes global variables and advances the pointer to after them + """ + code = "" + token = self.parser.current_token() + while token is not None and token.type in [Token.VOID, Token.INT, Token.SEMICOLON]: + if token.type == Token.SEMICOLON: # can have random semicolons ;) + self.parser.advance_token() + token = self.parser.current_token() + continue + self.parser.check_next_token_is(Token.ID) + + if self.parser.next_token(next_amount=2).type == Token.LPAREN: + function = self.create_function_object() + insert_function_object(function) + elif token.type is Token.INT and self.parser.next_token(next_amount=2).type in [ + Token.SEMICOLON, + Token.ASSIGN, + Token.LBRACK, + ]: + code += self.compile_global_variable_definition() + else: + raise BFSyntaxError( + "Unexpected '%s' after '%s'. Expected '(' (function definition) or one of: '=', ';', '[' (global variable definition)" + % (str(self.parser.next_token(next_amount=2)), str(self.parser.next_token())) + ) + + token = self.parser.current_token() + + if self.parser.current_token() is not None: # we have not reached the last token + untouched_tokens = [str(t) for t in self.parser.tokens[self.parser.current_token_index :]] + raise BFSyntaxError("Did not reach the end of the code. Untouched tokens:\n%s" % untouched_tokens) + + return code + + def compile(self): + insert_library_functions() + code = ( + self.process_global_definitions() + ) # code that initializes global variables and advances pointer to after them + + check_function_exists(Token(Token.ID, 0, 0, "main"), 0) + code += get_function_object("main").get_code(get_global_variables_size()) + code += "<" * get_global_variables_size() # point to the first cell to end the program nicely :) + return code + + +def compile(code, optimize_code=False): + """ + :param code: C-like code (string) + :param optimize_code: syntax optimization (bool) + :return code: Brainfuck code (string) + """ + compiler = Compiler(code, optimize_code) + brainfuck_code = compiler.compile() + return brainfuck_code + + +if __name__ == "__main__": + print("This file cannot be directly run") + print("Please import it and use the 'compile' function") + print("Which receives a C-like code (string) and returns Brainfuck code (string)") diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Exceptions.py b/reasoning_gym/code/contrib/bfit/Compiler/Exceptions.py new file mode 100644 index 00000000..5d11702c --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Exceptions.py @@ -0,0 +1,6 @@ +class BFSyntaxError(Exception): + pass + + +class BFSemanticError(Exception): + pass diff --git a/reasoning_gym/code/contrib/bfit/Compiler/FunctionCompiler.py b/reasoning_gym/code/contrib/bfit/Compiler/FunctionCompiler.py new file mode 100644 index 00000000..1603ac64 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/FunctionCompiler.py @@ -0,0 +1,1247 @@ +from collections import namedtuple +from functools import reduce + +from .Exceptions import BFSemanticError, BFSyntaxError +from .Functions import check_function_exists, get_function_object +from .General import ( + get_literal_token_value, + get_move_to_return_value_cell_code, + get_print_string_code, + get_variable_dimensions_from_token, + get_variable_from_ID_token, + is_token_literal, + process_switch_cases, +) +from .Globals import create_variable_from_definition, get_global_variables, get_variable_size, is_variable_array +from .Node import ( + NodeArrayAssignment, + NodeArrayGetElement, + NodeArraySetElement, + NodeFunctionCall, + NodeTernary, + NodeToken, + NodeUnaryPostfix, + NodeUnaryPrefix, +) +from .Parser import Parser +from .Token import Token + +""" +This file implements the FunctionCompiler object +This is where we actually compile the code - statements, assignments, calculations, etc +The syntax of the language is defined here as compilation rules + +A function is position-dependent - it needs to know where on the tape it runs +So that it can access global variables, which are at the beginning of the stack, correctly +Because of that, the function's code is dependent on when/where we call it +The idea is that we compile the function on demand - every time it is called +And every time we compile it - we pass the current stack pointer to it +This is implemented in the get_code() function + +The FunctionCompiler object holds tokens that correspond to the function so that we can compile it on demand +""" + + +class FunctionCompiler: + def __init__(self, name, tokens): + self.name = name + self.tokens = tokens + self.parser = Parser(self.tokens) + self.ids_map_list = list() + self.type = None + self.parameters = None + self.process_function_definition() # sets type and parameters + self.return_value_cell = None # will be set on every call to this function + + """ + ids_map_list is a list of named tuples. Each tuple represents a scope, and holds 2 items: + 1. an index of the next available cell. (if we want to insert a new ID to the ids_map_list, it will be in that index) + 2. a dictionary that maps an ID (string) to an index - the cell where we hold that variable + + We use this list as a stack: + when entering a scope, we insert a (available_cell, dictionary) to the BEGINNING of the list + when exiting a scope, we pop the last inserted tuple (the one at the BEGINNING of the list) + + When declaring a variable in the current scope, we add it to the dictionary at the beginning of the list, + and increase the 'next_available_cell' by 1 + When retrieving a variable, we go through the list and return the first occurrence that matches the ID + """ + + def process_function_definition(self): + # sets function type and parameters, advances parser + + function_return_type = self.parser.current_token() + self.parser.advance_token() # skip return type + function_name = self.parser.current_token().data + assert function_name == self.name + self.parser.advance_token() # skip ID + parameters = self.get_function_parameters_declaration() + # parser now points to LBRACE = beginning of function scope + + self.type = function_return_type + self.parameters = parameters + + def get_code(self, current_stack_pointer): + """ + layout: + current_stack_pointer ------- + | + v + global1 global2 unknown1 unknown2 my_return_value param1 param2 local1 local2 + + current_stack_pointer is current next available cell + which is the value of the caller's current_stack_pointer plus this function's parameters + create ids map for global variables + make room for return_value + """ + self.insert_global_variables_to_function_scope() + + # self.current_stack_pointer is now equal to the size of the global variables plus 1 (next_available_cell) + # new stack pointer should be at least that size + assert self.current_stack_pointer() <= current_stack_pointer + self.return_value_cell = current_stack_pointer + self.set_stack_pointer( + current_stack_pointer + 1 + ) # make room for return_value cell. next available cell is the next one after it. + function_code = self.compile_function_scope(self.parameters) + self.remove_ids_map() # Global variables + return function_code + + # ================= + # helper functions + # ================= + def insert_global_variables_to_function_scope(self): + self.add_ids_map() + for variable in get_global_variables(): + self.insert_to_ids_map(variable) + + def get_array_index_expression(self): + """ + the idea - address the multi-dimensional array as a one-dimensional array + calculate the appropriate index in the one-dimensional array + by multiplying the index in each dimension by its size (i.e the multiplication of all sizes of the following dimensions) + and then using the NodeArrayGetElement/NodeArraySetElement class which gets an element in a one-dimensional array + + in order to do that, we need to create our own sub-tree of multiplications, + and pass it as the "index expression" + + e.g if the array is: arr[10][5][2] and we want to get arr[4][3][1] + then we want to calculate index = (4*(5*2) + 3*(2) + 1) + """ + ID_token = self.parser.current_token() + self.parser.advance_token(2) # skip ID, LBRACK + first_index_expression = index_expression = self.expression() # first dimension + self.parser.check_current_token_is(Token.RBRACK) + self.parser.advance_token() # skip RBRACK + + # now handle the next dimensions (if multi-dimensional array) + dimensions = get_variable_dimensions_from_token(self.ids_map_list, ID_token) + if len(dimensions) > 1: + multiply_token = Token(Token.BINOP, ID_token.line, ID_token.column, data="*") + add_token = Token(Token.BINOP, ID_token.line, ID_token.column, data="+") + + # multiply by next dimensions sizes + multiply_amount = reduce(lambda x, y: x * y, dimensions[1:]) # size of the following dimensions + node_token_multiply_amount = NodeToken( + self.ids_map_list, token=Token(Token.NUM, ID_token.line, ID_token.column, data=str(multiply_amount)) + ) + index_expression = NodeToken( + self.ids_map_list, token=multiply_token, left=first_index_expression, right=node_token_multiply_amount + ) + + # handle next dimensions + dimension = 1 + while dimension < len(dimensions): + if self.parser.current_token().type != Token.LBRACK: # too few indexes given... + if dimension == 1: + return first_index_expression # allow use of only one dimension for multi-dimensional array + raise BFSemanticError( + "%s is a %s-dimensional array, but only %s dimension(s) given as index" + % (str(ID_token), len(dimensions), dimension) + ) + self.parser.check_current_token_is(Token.LBRACK) + self.parser.advance_token() # skip LBRACK + exp = self.expression() + + self.parser.check_current_token_is(Token.RBRACK) + self.parser.advance_token() # skip RBRACK + + # current_dimension_index *= size_of_following_dimensions + if dimension + 1 < len(dimensions): # not last dimension - need to multiply and add + multiply_amount = reduce( + lambda x, y: x * y, dimensions[dimension + 1 :] + ) # size of the following dimensions + node_token_multiply_amount = NodeToken( + self.ids_map_list, + token=Token(Token.NUM, ID_token.line, ID_token.column, data=str(multiply_amount)), + ) + multiply_node = NodeToken( + self.ids_map_list, token=multiply_token, left=exp, right=node_token_multiply_amount + ) + + # prev_dimensions_index += current_dimension_index + index_expression = NodeToken( + self.ids_map_list, token=add_token, left=index_expression, right=multiply_node + ) + else: # last dimension - no need to multiply, just add + index_expression = NodeToken(self.ids_map_list, token=add_token, left=index_expression, right=exp) + dimension += 1 + + if self.parser.current_token().type == Token.LBRACK: # too many indexes given... + raise BFSemanticError( + "%s is a %s-dimensional array. Unexpected %s" + % (str(ID_token), len(dimensions), self.parser.current_token()) + ) + return index_expression + + def get_token_after_array_access(self, offset=0): + # in case we have: "ID[a][b][c]...[z] next_token", return "next_token" + idx = self.parser.current_token_index + offset + self.parser.check_next_tokens_are([Token.ID, Token.LBRACK], starting_index=idx - 1) + idx += 1 # point to LBRACK + while self.parser.token_at_index(idx).type == Token.LBRACK: + idx = self.parser.find_matching(idx) # point to RBRACK + idx += 1 # advance to one after the RBRACK + + return self.parser.token_at_index(idx) + + def compile_array_assignment(self, token_id): + # int id[a][b][c]... = {1, 2, 3, ...}; + # or int id[a][b][c]... = "\1\2\3..."; + # or int id[a][b][c]... = {{1, 2}, {3, 4}, ...}; + # or array assignment: id = {1, 2, 3, ...}; + self.parser.check_current_token_is(Token.ASSIGN) + if self.parser.current_token().data != "=": + raise BFSyntaxError("Unexpected %s when assigning array. Expected ASSIGN (=)" % self.parser.current_token()) + + if self.parser.next_token().type not in [Token.LBRACE, Token.STRING]: + raise BFSyntaxError("Expected LBRACE or STRING at '%s'" % self.parser.next_token()) + + self.parser.advance_token() # skip to LBRACE or STRING + literal_tokens_list = self.parser.compile_array_initialization_list() + + return NodeArrayAssignment(self.ids_map_list, token_id, literal_tokens_list) + + def compile_variable_declaration(self): + self.parser.check_next_token_is(Token.ID) + self.parser.advance_token() # skip "INT" (now points to ID) + assert self.parser.current_token().type == Token.ID + + if self.parser.next_token().type == Token.SEMICOLON: # INT ID SEMICOLON + self.parser.advance_token(2) # skip ID SEMICOLON + return ( + "" # no code is generated here. code was generated for defining this variable when we entered the scope + ) + + elif ( + self.parser.next_token().type == Token.ASSIGN and self.parser.next_token().data == "=" + ): # INT ID = EXPRESSION SEMICOLON + return self.compile_expression_as_statement() # compile_expression_as_statement skips the SEMICOLON + + elif ( + self.parser.next_token().type == Token.LBRACK + ): # INT ID (LBRACK NUM RBRACK)+ (= ARRAY_INITIALIZATION)? SEMICOLON + # array definition (int arr[2][3]...[];) or array definition and initialization (arr[2][3]...[] = {...};) + token_id = self.parser.current_token() + self.parser.advance_token() # skip ID + while self.parser.current_token().type == Token.LBRACK: # loop to skip to after last RBRACK ] + self.parser.check_current_tokens_are([Token.LBRACK, Token.NUM, Token.RBRACK]) + self.parser.advance_token(3) # skip LBRACK, NUM, RBRACK + + if self.parser.current_token().type == Token.ASSIGN: # initialization + initialization_node = self.compile_array_assignment(token_id) + code = initialization_node.get_code(self.current_stack_pointer()) + "<" # discard expression value + else: + code = "" # just array definition + # no code is generated here. code was generated for defining this variable when we entered the scope + self.parser.check_current_token_is(Token.SEMICOLON) + self.parser.advance_token() # skip SEMICOLON + return code + else: + raise BFSyntaxError("Unexpected %s after %s" % (self.parser.next_token(), self.parser.current_token())) + + def add_ids_map(self): + """ + the first cells are global variable cells (index 0 to n) + the next cell (index n+1) is the return_value cell + every function assumes that these cells exist + """ + + next_available_cell = 0 if len(self.ids_map_list) == 0 else self.ids_map_list[0].next_available_cell + + ids_map = namedtuple("ids_map", ["next_available_cell", "IDs_dict"]) + ids_map.next_available_cell = next_available_cell + ids_map.IDs_dict = dict() + + self.ids_map_list.insert(0, ids_map) + + def remove_ids_map(self): + self.ids_map_list.pop(0) + + def insert_to_ids_map(self, variable): + ids_map = self.ids_map_list[0] + + self.check_id_doesnt_exist(variable.name) + + variable.cell_index = ids_map.next_available_cell + ids_map.next_available_cell += get_variable_size(variable) + ids_map.IDs_dict[variable.name] = variable + + def reserve_cell_in_ids_map(self): + """ + reserve cell by increasing the "pointer" of the next available cell + this is used for making room for return_value cell + """ + ids_map = self.ids_map_list[0] + ids_map.next_available_cell += 1 + + def variables_dict_size(self, variables_dict_index): + variables_dict = self.ids_map_list[variables_dict_index].IDs_dict + + size = 0 + for variable in variables_dict.values(): + size += get_variable_size(variable) + + return size + + def size_of_variables_current_scope(self): + return self.variables_dict_size(0) + + def size_of_global_variables(self): + return self.variables_dict_size(-1) + + def increase_stack_pointer(self, amount=1): + # sometimes it is needed to increase the stack pointer + # for example, when compiling "if ... else ...", we need 2 temporary cells before the inner scope code of both the if and the else + # another example - when evaluating expression list in function call, each expression is evaluated while pointing to a different cell + # therefore, it is needed to "update" the stack pointer to represent the new pointer + self.ids_map_list[0].next_available_cell += amount + + def decrease_stack_pointer(self, amount=1): + self.ids_map_list[0].next_available_cell -= amount + + def set_stack_pointer(self, new_value): + assert new_value >= self.ids_map_list[0].next_available_cell + self.ids_map_list[0].next_available_cell = new_value + + def current_stack_pointer(self): + return self.ids_map_list[0].next_available_cell + + def insert_scope_variables_into_ids_map(self): + # go through all the variable definitions in this scope (not including sub-scopes), and add them to the ids map + # move the pointer to the next available cell (the one after the last variable declared in this scope) + + assert self.parser.current_token().type == Token.LBRACE + self.parser.advance_token() + + i = self.parser.current_token_index + while i < len(self.tokens): + token = self.tokens[i] + + if token.type == Token.INT: + if ( + self.tokens[i - 2].type != Token.FOR + ): # if it is not a definition inside a FOR statement (for (int i = 0...)) + variable = create_variable_from_definition(self.parser, index=i) + self.insert_to_ids_map(variable) + + elif token.type == Token.LBRACE: + i = self.parser.find_matching(starting_index=i) + + elif token.type == Token.RBRACE: + break # we have reached the end of the scope + + i += 1 + + return ">" * self.size_of_variables_current_scope() # advance pointer to the next available cell + + def enter_scope(self): + # create an ids map to the current scope, and then inserts the scope variables into it + self.add_ids_map() + return self.insert_scope_variables_into_ids_map() + + def exit_scope(self): + # remove the ids map of the current scope + # return pointer to the previous scope's next available cell + code = "<" * self.size_of_variables_current_scope() + self.remove_ids_map() + return code + + def enter_function_scope(self, parameters): + # make room for return_value cell + # create an ids map to the current function scope + # insert parameters into the ids map + # insert scope variables into the ids map + + self.add_ids_map() + for parameter in parameters: + self.insert_to_ids_map(parameter) + + code = ">" # skip return_value_cell + code += self.insert_scope_variables_into_ids_map() + # this inserts scope variables AND moves pointer right, with the amount of BOTH parameters and scope variables + + return code + + def check_id_doesnt_exist(self, ID): + # make sure that the id does not exist in the current scope + # used when defining a variable + if ID in self.ids_map_list[0].IDs_dict: + raise BFSemanticError("ID %s is already defined" % ID) + + # ================= + # compilation rules + # ================= + + # expression + def function_call(self): + # function_call: ID LPAREN expression_list RPAREN + # returns NodeFunctionCall + assert self.parser.current_token().type == Token.ID + + function_token = self.parser.current_token() + function_name = function_token.data + self.parser.advance_token() # skip ID + + if function_name == self.name: + raise BFSemanticError("No support for recursion yet :(.... in function call '%s'" % str(function_token)) + + parameters = self.compile_expression_list() + + check_function_exists(function_token, len(parameters)) + function_to_call = get_function_object(function_name) + + return NodeFunctionCall(self.ids_map_list, function_to_call, parameters) + + def literal(self): + # literal: NUM | CHAR | ID | ID (LBRACK expression RBRACK)+ | TRUE | FALSE | function_call | ( expression ) + + token = self.parser.current_token() + + if token.type == Token.ID and self.parser.next_token().type == Token.LPAREN: + return self.function_call() + + if ( + token.type == Token.ID and self.parser.next_token().type == Token.LBRACK + ): # array - ID(LBRACK expression RBRACK)+ + index_expression = self.get_array_index_expression() + return NodeArrayGetElement(self.ids_map_list, token, index_expression) + + if is_token_literal(token) or token.type == Token.ID: + self.parser.advance_token() + return NodeToken(self.ids_map_list, token=token) + + if token.type != Token.LPAREN: + raise BFSyntaxError( + "Unexpected '%s'. expected literal (NUM | ID | ID(LBRACK expression RBRACK)+ | TRUE | FALSE | function_call | ( expression ))" + % str(token) + ) + + # ( expression ) + self.parser.check_current_token_is(Token.LPAREN) + self.parser.advance_token() # skip LPAREN + exp = self.expression() + self.parser.check_current_token_is(Token.RPAREN) + self.parser.advance_token() # skip RPAREN + + return exp + + def unary_postfix(self): + # unary_postfix: literal ( ++ | -- | UNARY_MULTIPLICATIVE)? + + literal = self.literal() + token = self.parser.current_token() + + if token.type in [Token.INCREMENT, Token.DECREMENT, Token.UNARY_MULTIPLICATIVE]: + self.parser.advance_token() + new_node = NodeUnaryPostfix(self.ids_map_list, operation=token, literal=literal) + return new_node + else: + return literal + + def unary_prefix(self): + # unary_prefix: ( (!|+|-)* unary_prefix ) | ( ( ++ | -- | UNARY_MULTIPLICATIVE | ~ ) literal ) | unary_postfix + + token = self.parser.current_token() + + if token.type in [Token.NOT, Token.BITWISE_NOT, Token.BINOP]: + if token.type == Token.BINOP and token.data not in ["+", "-"]: + raise BFSyntaxError( + "Expected either + or - as unary prefix instead of token %s" % self.parser.current_token() + ) + self.parser.advance_token() + unary_prefix = self.unary_prefix() + + new_node = NodeUnaryPrefix(self.ids_map_list, operation=token, literal=unary_prefix) + return new_node + + elif token.type in [Token.INCREMENT, Token.DECREMENT, Token.UNARY_MULTIPLICATIVE]: + self.parser.advance_token() + literal = self.literal() + + new_node = NodeUnaryPrefix(self.ids_map_list, operation=token, literal=literal) + return new_node + + else: + return self.unary_postfix() + + def multiplicative(self): + # multiplicative: unary_prefix ((MUL|DIV|MOD) unary_prefix)* + + n = self.unary_prefix() + + token = self.parser.current_token() + while token is not None and token.type == Token.BINOP and token.data in ["*", "/", "%"]: + self.parser.advance_token() + next_factor = self.unary_prefix() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_factor) + n = new_node + + token = self.parser.current_token() + + return n + + def additive(self): + # additive: multiplicative ((PLUS|MINUS) multiplicative)* + + n = self.multiplicative() + + token = self.parser.current_token() + while token is not None and token.type == Token.BINOP and token.data in ["+", "-"]: + self.parser.advance_token() + next_term = self.multiplicative() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_term) + n = new_node + + token = self.parser.current_token() + + return n + + def shift(self): + # shift: additive (<<|>> additive)* + + n = self.additive() + + token = self.parser.current_token() + while token is not None and token.type == Token.BITWISE_SHIFT: + self.parser.advance_token() + next_additive = self.additive() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_additive) + n = new_node + + token = self.parser.current_token() + + return n + + def relational(self): + # relational: shift (==|!=|<|>|<=|>= shift)? + + a = self.shift() + + token = self.parser.current_token() + if token.type != Token.RELOP: # just an arithmetic expression + return a + + self.parser.advance_token() + b = self.shift() + + new_node = NodeToken(self.ids_map_list, token=token, left=a, right=b) + return new_node + + def bitwise_and(self): + # bitwise_and: relational (& relational)* + + n = self.relational() + + token = self.parser.current_token() + while token is not None and token.type == Token.BITWISE_AND: + self.parser.advance_token() + next_relational = self.relational() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_relational) + n = new_node + + token = self.parser.current_token() + + return n + + def bitwise_xor(self): + # bitwise_xor: bitwise_and (| bitwise_and)* + + n = self.bitwise_and() + + token = self.parser.current_token() + while token is not None and token.type == Token.BITWISE_XOR: + self.parser.advance_token() + next_bitwise_and = self.bitwise_and() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_bitwise_and) + n = new_node + + token = self.parser.current_token() + + return n + + def bitwise_or(self): + # bitwise_or: bitwise_xor (| bitwise_xor)* + + n = self.bitwise_xor() + + token = self.parser.current_token() + while token is not None and token.type == Token.BITWISE_OR: + self.parser.advance_token() + next_bitwise_xor = self.bitwise_xor() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_bitwise_xor) + n = new_node + + token = self.parser.current_token() + + return n + + def logical_and(self): + # logical_and: bitwise_or (&& bitwise_or)* + + n = self.bitwise_or() + + token = self.parser.current_token() + while token is not None and token.type == Token.AND: + self.parser.advance_token() + next_bitwise_or = self.bitwise_or() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_bitwise_or) + n = new_node + + token = self.parser.current_token() + + return n + + def logical_or(self): + # logical_or: logical_and (|| logical_and)* + + n = self.logical_and() + + token = self.parser.current_token() + while token is not None and token.type == Token.OR: + self.parser.advance_token() + next_and = self.logical_and() + + new_node = NodeToken(self.ids_map_list, token=token, left=n, right=next_and) + n = new_node + + token = self.parser.current_token() + + return n + + def ternary_expression(self): + # ternary_expression: logical_or (? expression : ternary_expression)? + n = self.logical_or() + if self.parser.current_token().type != Token.TERNARY: + return n + + self.parser.advance_token() # skip ? + node_true = self.expression() + self.parser.check_current_token_is(Token.COLON) + self.parser.advance_token() # skip : + node_false = self.ternary_expression() + return NodeTernary(self.ids_map_list, n, node_true, node_false) + + def assignment(self): + # assignment: ID ASSIGN expression | ID ASSIGN ARRAY_INITIALIZATION | ID (LBRACK expression RBRACK)+ ASSIGN expression | ternary_expression + + if self.parser.current_token().type == Token.ID and self.parser.next_token().type == Token.ASSIGN: + + if self.parser.next_token(2).type in [Token.LBRACE, Token.STRING]: # ID ASSIGN ARRAY_INITIALIZATION + token_ID = self.parser.current_token() + self.parser.advance_token() # skip ID + variable_ID = get_variable_from_ID_token(self.ids_map_list, token_ID) + if not is_variable_array(variable_ID): + raise BFSemanticError("Trying to assign array to non-array variable %s" % token_ID) + return self.compile_array_assignment(token_ID) + + # ID ASSIGN expression + id_token = self.parser.current_token() + assign_token = self.parser.next_token() + self.parser.advance_token(amount=2) # skip ID ASSIGN + + expression_node = self.expression() + + new_node = NodeToken( + self.ids_map_list, + left=NodeToken(self.ids_map_list, token=id_token), + token=assign_token, + right=expression_node, + ) + return new_node + + elif ( + self.parser.current_token().type == Token.ID + and self.parser.next_token().type == Token.LBRACK + and self.get_token_after_array_access().type == Token.ASSIGN + ): + # ID (LBRACK expression RBRACK)+ ASSIGN value_expression + id_token = self.parser.current_token() + index_expression = self.get_array_index_expression() + self.parser.check_current_token_is(Token.ASSIGN) + assign_token = self.parser.current_token() + self.parser.advance_token() # skip ASSIGN + value_expression = self.expression() + + return NodeArraySetElement(self.ids_map_list, id_token, index_expression, assign_token, value_expression) + else: + return self.ternary_expression() + + def expression(self): + # expression: assignment + return self.assignment() + + def compile_expression(self): + # parses mathematical expressions (+-*/ ()) + # increments/decrements (++, --) + # relative operations (==, !=, <, >, <=, >=) + # bitwise operations (|, &, ^, ~) + # logical operations (!, &&, ||, ~) + # ternary expression (?) + # assignment (=, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=) + # this is implemented using a Node class that represents a parse tree + + """ + (used reference: https://introcs.cs.princeton.edu/java/11precedence/) + order of operations (lowest precedence to highest precedence) + assignment (=, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=) + ternary_expression (?) + logical_or (||) + logical_and (&&) + bitwise_or (|) + bitwise_xor (^) + bitwise_and (&) + bitwise_not (~) + relational (==|!=|<|>|<=|>=) + shift (<<|>>) + additive (+-) + multiplicative (*/%) + unary_prefix (!, ++, --, ~) + unary_postfix (++, --) + + expression: assignment + assignment: ID (=|+=|-=|*=|/=|%=|<<=|>>=|&=|(|=)|^=) expression | ternary_expression + ternary_expression: logical_or (? expression : ternary_expression)? + logical_or: logical_and (|| logical_and)* + logical_and: bitwise_or (&& bitwise_or)* + bitwise_or: bitwise_xor (| bitwise_xor)* + bitwise_xor: bitwise_and (^ bitwise_and)* + bitwise_and: relational (& relational)* + relational: shift (==|!=|<|>|<=|>= shift)? + shift: additive ((<<|>>) additive)* + additive: multiplicative ((PLUS|MINUS) multiplicative)* + multiplicative: unary_prefix ((MUL|DIV|MOD) unary_prefix)* + unary_prefix: ( (!|+|-)* unary_prefix ) | ( ( ++ | -- | ~ ) literal ) | unary_postfix + unary_postfix: literal ( ++ | -- )? + literal: NUM | CHAR | ID | ID[expression] | TRUE | FALSE | function_call | ( expression ) + """ + + parse_tree = self.expression() + expression_code = parse_tree.get_code(self.current_stack_pointer()) + return expression_code + + # functions-related + def get_function_parameters_declaration(self): + # parameters declaration: LPAREN (int ID (LBRACK NUM RBRACK)? (COMMA int ID)*)? RPAREN + # return list of parameters (named tuples (type, ID)) at the same order as declared + + assert self.parser.current_token().type == Token.LPAREN + self.parser.advance_token() + + res = list() + + token = self.parser.current_token() + while token.type != Token.RPAREN: + if token.type != Token.INT: + raise BFSemanticError("Only int type is supported as a function parameter, and not '%s'" % str(token)) + + parameter = create_variable_from_definition(self.parser, advance_tokens=True) + res.append(parameter) + + if self.parser.current_token().type == Token.COMMA: + self.parser.advance_token() + else: + self.parser.check_current_token_is(Token.RPAREN) + + token = self.parser.current_token() + + self.parser.advance_token() # skip RPAREN + return res + + def compile_expression_list(self): + # expression_list: ( expression (COMMA expression)* )? + # returns a list of Nodes - one node for each expression + assert self.parser.current_token().type == Token.LPAREN + self.parser.advance_token() + + expressions = list() + + token = self.parser.current_token() + while token.type != Token.RPAREN: + expressions.append(self.expression()) + + if self.parser.current_token().type == Token.COMMA: + self.parser.advance_token() + else: + self.parser.check_current_token_is(Token.RPAREN) + token = self.parser.current_token() + + self.parser.advance_token() # skip RPAREN + return expressions + + def compile_return(self): + # this assumes that the return is the last statement in the function + + self.parser.advance_token() # skip return + if self.parser.current_token().type == Token.SEMICOLON: + # return; + self.parser.advance_token() # skip ; + return "" # nothing to do + + # return exp; + expression_code = self.compile_expression() + self.parser.check_current_token_is(Token.SEMICOLON) + + self.parser.advance_token() # skip ; + + code = expression_code # after this, we point to next available cell + code += "<" # point to value to return + code += get_move_to_return_value_cell_code(self.return_value_cell, self.current_stack_pointer()) + + return code + + # statements + def compile_expression_as_statement(self): + # this expression can be used as a statement. + # e.g: x+=5; or x++ or ++x; + + assert self.parser.current_token().type in [ + Token.ID, + Token.INCREMENT, + Token.DECREMENT, + Token.UNARY_MULTIPLICATIVE, + ] + + code = self.compile_expression() + self.parser.check_current_token_is(Token.SEMICOLON) + self.parser.advance_token() # skip ; + + code += "<" # discard the expression's value + + return code + + def compile_print_string(self): + # print(string); + self.parser.check_next_tokens_are([Token.LPAREN, Token.STRING, Token.RPAREN, Token.SEMICOLON]) + self.parser.advance_token(amount=2) # skip print ( + string_to_print = self.parser.current_token().data + self.parser.advance_token(amount=3) # skip string ) ; + + code = get_print_string_code(string_to_print) + return code + + def compile_function_call_statement(self): + # compile statement: function_call SEMICOLON + function_call_node = self.function_call() + function_call_code = function_call_node.get_code(current_pointer=self.current_stack_pointer()) + + self.parser.check_current_token_is(Token.SEMICOLON) + self.parser.advance_token() # skip ; + + code = function_call_code # at this point, we point to one after the return value + code += "<" # discard return value + return code + + def compile_if(self): + # if (expression) statement (else statement)? note - statement can be scope { } + + self.parser.check_next_token_is(Token.LPAREN) + self.parser.advance_token(amount=2) # skip to after LPAREN + + expression_code = self.compile_expression() + self.parser.check_current_token_is(Token.RPAREN) + self.parser.advance_token() # point to after RPAREN + + # if ... (else ...)? + # need to use 2 temp cells + # expression, execute_else + + self.increase_stack_pointer(amount=2) + inside_if_code = self.compile_statement() + + have_else = self.parser.current_token().type == Token.ELSE + if have_else: + self.parser.advance_token() # skip the 'else' + inside_else_code = self.compile_statement() + self.decrease_stack_pointer(amount=2) + + code = expression_code # evaluate expression. after this we point to "execute_else" cell + if have_else: + code += "[-]+" # execute_else = 1 + code += "<" # point to the expression + code += "[" # if it is non-zero + code += ">" # point to execute_else + if have_else: + code += "-" # execute_else = 0 + code += ">" # point to next available cell + code += inside_if_code # after this we point to the same cell (one after execute_else) + code += "<<" # point to expression + code += "[-]" # expression = 0 + code += "]" # end if + # now we point to next available cell (what used to be expression_code) + + if have_else: + code += ">" # point to execute_else + code += "[" # if it is non-zero + code += ">" # point to next available cell + code += inside_else_code # after this we point to the same cell (one after execute_else) + code += "<" # point to execute_else + code += "-" # execute_else = 0 + code += "]" # end if + code += "<" # point to next available cell (what used to be expression_code) + + return code + + def compile_while(self): # while (expression) statement note - statement can be scope { } + self.parser.check_next_token_is(Token.LPAREN) + self.parser.advance_token(amount=2) # skip to after LPAREN + + expression_code = self.compile_expression() + + self.parser.check_current_token_is(Token.RPAREN) + self.parser.advance_token() # point to after RPAREN + + inner_scope_code = self.compile_statement() + + code = expression_code # evaluate expression + code += "<" # point to the expression + code += "[" # if it is 0, jump to after the scope + code += inner_scope_code # scope code. after this code, pointer points to the next available cell. i.e one after the expression + code += expression_code # re-evaluate the expression + code += "<" # point to the expression + code += "]" # after scope + + return code + + def compile_do_while(self): # do statement while (expression) semicolon note - statement can be scope { } + self.parser.check_current_token_is(Token.DO) + self.parser.advance_token() + + inner_scope_code = self.compile_statement() + + self.parser.check_current_tokens_are([Token.WHILE, Token.LPAREN]) + self.parser.advance_token(amount=2) # point to after LPAREN + + expression_code = self.compile_expression() + + self.parser.check_current_tokens_are([Token.RPAREN, Token.SEMICOLON]) + self.parser.advance_token(amount=2) # point to after SEMICOLON + + code = "[-]+" # set expression to 1. since do while loops executes the scope code first. + code += "[" # go in scope + code += inner_scope_code # scope code. after this code, pointer points to the same cell. i.e the expression + code += expression_code # evaluate the expression, after this code, the pointer is pointing to the next cell + code += "<" # point to the expression + code += "]" # after scope + + return code + + def compile_switch(self): # switch (expression) { ((default | case literal): statements* break;? statements*)* } + self.parser.check_current_tokens_are([Token.SWITCH, Token.LPAREN]) + self.parser.advance_token(amount=2) # point to after LPAREN + + self.increase_stack_pointer() # use 1 temp cell before evaluating the expression + expression_code = self.compile_expression() + self.parser.check_current_tokens_are([Token.RPAREN, Token.LBRACE]) + self.parser.advance_token(amount=2) # point to after LBRACE + + self.increase_stack_pointer() # use 1 additional temp cell for indicating we need to execute a case + cases = list() # list of tuples: (value/"default" (int or string), case_code (string), has_break(bool)) + + while self.parser.current_token().type in [ + Token.CASE, + Token.DEFAULT, + ]: # (default | CASE literal) COLON statement* break;? statements* + if self.parser.current_token().type == Token.CASE: + self.parser.advance_token() # skip CASE + constant_value_token = self.parser.current_token() + if not is_token_literal(constant_value_token): + raise BFSemanticError("Switch case value is not a literal. Token is %s" % constant_value_token) + + value = get_literal_token_value(constant_value_token) + if value in [case for (case, _, _) in cases]: + raise BFSemanticError("Case %d already exists. Token is %s" % (value, constant_value_token)) + else: + assert self.parser.current_token().type == Token.DEFAULT + value = "default" + if value in [case for (case, _, _) in cases]: + raise BFSemanticError("default case %s already exists." % self.parser.current_token()) + + self.parser.check_next_token_is(Token.COLON) + self.parser.advance_token(amount=2) # point to after COLON + + inner_case_code = "" + while self.parser.current_token().type not in [Token.CASE, Token.DEFAULT, Token.RBRACE, Token.BREAK]: + inner_case_code += self.compile_statement( + allow_declaration=False + ) # not allowed to declare variables directly inside case + + has_break = False + if self.parser.current_token().type == Token.BREAK: # ignore all statements after break + self.parser.check_next_token_is(Token.SEMICOLON) + self.parser.advance_token(amount=2) # skip break SEMICOLON + has_break = True + while self.parser.current_token().type not in [Token.CASE, Token.DEFAULT, Token.RBRACE]: + self.compile_statement() # advance the parser and discard the code + cases.append((value, inner_case_code, has_break)) + + if self.parser.current_token().type not in [Token.CASE, Token.DEFAULT, Token.RBRACE]: + raise BFSyntaxError( + "Expected case / default / RBRACE (}) instead of token %s" % self.parser.current_token() + ) + self.parser.check_current_token_is(Token.RBRACE) + self.parser.advance_token() + self.decrease_stack_pointer(amount=2) + + return process_switch_cases(expression_code, cases) + + def compile_break(self): + # TODO: Make the break statement in scopes inside switch-case (including if/else), and for/do/while + raise NotImplementedError( + "Break statement found outside of switch case first scope.\nBreak is not currently implemented for while/for/do statements.\nToken is %s" + % self.parser.current_token() + ) + + def compile_for(self): + # for (statement expression; expression) inner_scope_code note: statement contains ;, and inner_scope_code can be scope { } + # (the statement/second expression/inner_scope_code can be empty) + # (the statement cannot contain scope - { and } ) + + """ + is a special case of scope + the initial code (int i = 0;) is executed INSIDE the scope, but BEFORE the LBRACE + so we manually compile the scope instead of using self.compile_scope(): + + we first create an ids map, and in the case that there is a variable definition inside the definition: + we manually insert the ID into the ids map, and move the pointer to the right once, to make room for it + (this needs to be done before the definition's statement) + next, inside the for's scope {}: + after calling insert_scope_variables_into_ids_map, we move the pointer to the left once, since it counts the ID we entered manually as well + after calling exit_scope, we move the pointer to the right, since it counts the ID we entered manually, and we don't want it to be discarded after every iteration + finally, at the end of the loop, we move the pointer once to the left, to discard the variable we defined manually + """ + + self.parser.check_current_tokens_are([Token.FOR, Token.LPAREN]) + self.parser.advance_token(amount=2) # skip for ( + + manually_inserted_variable_in_for_definition = False + variable = None + code = "" + + # =============== enter FOR scope =============== + self.add_ids_map() + # =============================================== + + if self.parser.current_token().type == Token.INT: + # we are defining a variable inside the for statement definition (for (int i = 0....)) + variable = create_variable_from_definition(self.parser, advance_tokens=False) + self.insert_to_ids_map(variable) + manually_inserted_variable_in_for_definition = True + code += ">" * get_variable_size(variable) + + show_side_effect_warning = self.parser.next_token(2).type != Token.ASSIGN + if self.parser.next_token(2).type == Token.LBRACK: + show_side_effect_warning = self.get_token_after_array_access(offset=1).type != Token.ASSIGN + + if show_side_effect_warning: + print( + "[Warning] For loop variable '%s' isn't assigned to anything and may cause side effects" + % self.parser.next_token() + ) + + if self.parser.current_token().type == Token.LBRACE: # statement is a scope + raise BFSyntaxError("Unexpected scope inside for loop statement - %s" % self.parser.current_token()) + initial_statement = self.compile_statement() + + condition_expression = self.compile_expression() + self.parser.check_current_token_is(Token.SEMICOLON) + self.parser.advance_token() # skip ; + + if self.parser.current_token().type == Token.RPAREN: + modification_expression = "" # no modification expression + else: + modification_expression = self.compile_expression() + modification_expression += "<" # discard expression value + self.parser.check_current_token_is(Token.RPAREN) + self.parser.advance_token() # skip ) + + inner_scope_code = "" + if self.parser.current_token().type == Token.LBRACE: # do we have {} as for's statement? + # compiling scope inside { }: + if manually_inserted_variable_in_for_definition: + inner_scope_code += "<" * get_variable_size(variable) + inner_scope_code += self.insert_scope_variables_into_ids_map() + inner_scope_code += self.compile_scope_statements() + else: + inner_scope_code += self.compile_statement() + # =============== exit FOR scope =============== + inner_scope_code += self.exit_scope() + if manually_inserted_variable_in_for_definition: + inner_scope_code += ">" * get_variable_size(variable) + # ============================================== + + code += initial_statement + code += condition_expression # evaluate expression + code += "<" # point to the expression + code += "[" # if it is 0, jump to after the scope + code += inner_scope_code # scope code + code += modification_expression + code += condition_expression # re-evaluate the expression + code += "<" # point to the expression + code += "]" # after scope + + if manually_inserted_variable_in_for_definition: + code += "<" * get_variable_size(variable) + + return code + + def compile_statement(self, allow_declaration=True): + # returns code that performs the current statement + # at the end, the pointer points to the same location it pointed before the statement was executed + + token = self.parser.current_token() + if token.type == Token.INT: # INT ID ((= EXPRESSION) | ([NUM])+ (= ARRAY_INITIALIZATION)?)? SEMICOLON + if not allow_declaration: + raise BFSemanticError( + "Cannot define variable (%s) directly inside case. " + "Can define inside new scope {} or outside the switch statement" % token + ) + return self.compile_variable_declaration() + + elif token.type in [Token.INCREMENT, Token.DECREMENT, Token.UNARY_MULTIPLICATIVE]: # ++ID; + return self.compile_expression_as_statement() + + elif token.type == Token.ID: + if self.parser.next_token().type in [ + Token.ASSIGN, + Token.LBRACK, + Token.INCREMENT, + Token.DECREMENT, + Token.UNARY_MULTIPLICATIVE, + ]: + # ID ASSIGN expression; or ID([expression])+ ASSIGN expression; or ID++; + return self.compile_expression_as_statement() + elif self.parser.next_token().type == Token.LPAREN: # ID(...); (function call) + return self.compile_function_call_statement() + raise BFSyntaxError( + "Unexpected '%s' after '%s'. Expected '=|+=|-=|*=|/=|%%=|<<=|>>=|&=|(|=)|^=' (assignment), '++|--' (modification) or '(' (function call)" + % (str(self.parser.next_token()), str(token)) + ) + + elif token.type == Token.PRINT: + return self.compile_print_string() + + elif token.type == Token.IF: + return self.compile_if() + + elif token.type == Token.LBRACE: + return self.compile_scope() + + elif token.type == Token.WHILE: + return self.compile_while() + + elif token.type == Token.DO: + return self.compile_do_while() + + elif token.type == Token.SWITCH: + return self.compile_switch() + + elif token.type == Token.BREAK: + return self.compile_break() + + elif token.type == Token.RETURN: + return self.compile_return() + + elif token.type == Token.FOR: + return self.compile_for() + + elif token.type == Token.SEMICOLON: + # empty statement + self.parser.advance_token() # skip ; + return "" + + elif token.type in [Token.CASE, Token.DEFAULT]: + raise BFSyntaxError("%s not inside a switch statement" % token) + + raise BFSyntaxError("Invalid statement at " + str(token)) + + def compile_scope_statements(self): + tokens = self.tokens + + code = "" + while self.parser.current_token() is not None: + if self.parser.current_token().type == Token.RBRACE: + # we reached the end of our scope + self.parser.advance_token() # skip RBRACE + return code + else: + code += self.compile_statement() + + # should never get here + raise BFSyntaxError("expected } after the last token in scope " + str(tokens[-1])) + + def compile_scope(self): + assert self.parser.current_token().type == Token.LBRACE + + code = self.enter_scope() + code += self.compile_scope_statements() + code += self.exit_scope() + + return code + + def compile_function_scope(self, parameters): + # returns code for the current function + # parameters is a list of parameters, in the order of their declaration + # will be inserted into the new scope prior to the scope's compilation + + """ + example layout: + int global_var1; + int global_var2; + int foo(int a, int b) { + int x; + int y; + return 5; + } + + int main() { + int n; + foo(1, 2); + } + + global_var1 global_var2 main_return_value n foo_return_value a=1 b=2 x y + + calling convention: + caller responsibility: make room for return_value (and zero its cell), place parameters, point to return_value cell + callee responsibility: put return value in return_value cell and point to it (thus "cleaning" parameters) + can assume that there is a zeroed cell at current_stack_pointer (return_value_cell) (therefore ids_map starts at index current_stack_pointer+1) + can assume that the next cells match your parameters + assumes that initially, the pointer points to the first cell (return_value_cell). + therefore begin with '>' * (1 + parameters + scope variables) + """ + + assert self.parser.current_token().type == Token.LBRACE + + code = self.enter_function_scope(parameters) + code += self.compile_scope_statements() + code += self.exit_scope() + code += "<" # point to return_value_cell + + return code diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Functions.py b/reasoning_gym/code/contrib/bfit/Compiler/Functions.py new file mode 100644 index 00000000..a4ed4b9a --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Functions.py @@ -0,0 +1,37 @@ +from copy import deepcopy + +from .Exceptions import BFSemanticError + +functions = dict() # Global dictionary of function_name --> FunctionCompiler objects + + +def insert_function_object(function): + functions[function.name] = function + + +def get_function_object(name): + """ + must return a copy of the function + because we might need to compile function recursively + and if we don't work on different copies then we will interfere with the current token pointer etc + + for example: + int increase(int n) { return n+1;} + int main() {int x = increase(increase(1));} + + while compiling the first call, we start a compilation of the same function object in the second call + """ + return deepcopy(functions[name]) + + +def check_function_exists(function_token, parameters_amount): + function_name = function_token.data + if function_name not in functions: + raise BFSemanticError("Function '%s' is undefined" % str(function_token)) + + function = functions[function_name] + if len(function.parameters) != parameters_amount: + raise BFSemanticError( + "Function '%s' has %s parameters (called it with %s parameters)" + % (str(function_token), len(function.parameters), parameters_amount) + ) diff --git a/reasoning_gym/code/contrib/bfit/Compiler/General.py b/reasoning_gym/code/contrib/bfit/Compiler/General.py new file mode 100644 index 00000000..6abab5cf --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/General.py @@ -0,0 +1,1274 @@ +from functools import reduce + +from .Exceptions import BFSemanticError, BFSyntaxError +from .Token import Token + +""" +This file holds functions that generate general Brainfuck code +And general functions that are not dependent on other objects +""" + + +# ================= +# Brainfuck code +# ================= + + +def get_set_cell_value_code(new_value, previous_value, zero_next_cell_if_necessary=True): + # this function returns a code that sets the current cell's value to new_value, + # given that its previous value is previous_value + + # it may return the "naive" way, of "+"/"-" usage, times + # and it may return an optimization using loops, by using the next cell as a loop counter + # if zero_next_cell_if_necessary is set to False, it assumes that the next cell is already 0 + + # after the code of this function is executed, the pointer will point to the original cell + # this function returns the shorter code between "naive" and "looped" + + def get_char(value): + return "+" if value > 0 else "-" + + offset = new_value - previous_value + char = get_char(offset) + is_negative = offset < 0 + offset = abs(offset) + + # "naive" code is simply +/-, times + naive = char * offset + + # "looped" code is "[ times perform adds/subs] and then more adds/subs" + def get_abc(offset): + # returns a,b,c such that a*b+c=offset and a+b+c is minimal + + min_a, min_b, min_c = offset, 1, 0 + min_sum = offset + 1 + + left = 1 + right = offset // 2 - 1 + + while right >= left: + a, b = left + 1, right + 1 + c = offset - a * b + curr_sum = abs(a) + abs(b) + abs(c) + + if curr_sum < min_sum: + min_a, min_b, min_c = a, b, c + min_sum = curr_sum + + if a * b > offset: + right -= 1 + else: + left += 1 + + return min_a, min_b, min_c + + a, b, c = get_abc(offset) + looped = ">" # point to next cell (loop counter) + if zero_next_cell_if_necessary: + looped += "[-]" # zero it if necessary + looped += "+" * a # set loop counter + looped += "[<" + char * abs(b) + ">-]" # sub 1 from counter, perform b actions + looped += "<" # point to "character" cell + looped += get_char(-c if is_negative else c) * abs(c) # c more actions + + if len(naive) > len(looped): + return looped + else: + return naive + + +def get_move_to_offset_code(offset): + # returns code that moves value from current pointer to cell at offset to the left + # after this, the pointer points to the original cell, which is now the next available cell + + code = "<" * offset # point to destination + code += "[-]" # zero destination + code += ">" * offset # point to source cell + code += "[" + "<" * offset + "+" + ">" * offset + "-]" # increase destination, zero source + # point to next free location (source, which is now zero) + + return code + + +def get_copy_to_offset_code(offset): + # returns code that copies value from current pointer to cell at offset to the left + # after this, the pointer points to the original cell, which remains unchanged + + code = ">" # point to temp + code += "[-]" # zero temp + code += "<" * (offset + 1) # point to destination + code += "[-]" # zero destination + code += ">" * offset # point to source cell + code += "[>+" + "<" * (offset + 1) + "+" + ">" * offset + "-]" # increase temp and destination, zero source + code += ">" # point to temp + code += "[<+>-]" # move temp to original cell + code += "<" # point to original cell + + return code + + +def get_copy_to_variable_code(ids_map_list, ID_token, current_pointer): + # returns code that copies value from current pointer to cell of the variable ID + # after this, the pointer points to the original cell, which remains unchanged + + offset = get_offset_to_variable(ids_map_list, ID_token, current_pointer) + return get_copy_to_offset_code(offset) + + +def get_move_to_return_value_cell_code(return_value_cell, current_stack_pointer): + # returns code that moves value from current pointer to return_value cell + # after this, the pointer points to the original cell, which is now the next available cell + + # we need to move it cells left + return get_move_to_offset_code(current_stack_pointer - return_value_cell) + + +def unpack_multidimensional_literal_tokens_to_array_dimensions(ID_token, array_dimensions, literal_tokens_list): + if len(array_dimensions) == 0: + raise BFSemanticError("Tried to initialize array %s with too many nested sub-arrays" % ID_token) + if len(literal_tokens_list) > array_dimensions[0]: + raise BFSemanticError( + "Tried to initialize array %s dimension %s with too many elements (%s)" + % (ID_token, str(array_dimensions), str(len(literal_tokens_list))) + ) + + result = [] + for element in literal_tokens_list: + if isinstance(element, list): + # recursively unpack the list with the sub-dimension of the sub-array + # E.g if we have arr[3][3][3] and then this call will fill [3][3]=9 elements + result.extend( + unpack_multidimensional_literal_tokens_to_array_dimensions(ID_token, array_dimensions[1:], element) + ) + else: + result.append(element) + if len(array_dimensions) > 1: + dimension_size = dimensions_to_size(array_dimensions[1:]) # current size we need to fill + result.extend( + [Token(Token.NUM, 0, 0, "0")] * (dimension_size - 1) + ) # fill missing elements in this dimension with zeros + + dimension_size = dimensions_to_size(array_dimensions) # current size we need to fill + result.extend([Token(Token.NUM, 0, 0, "0")] * (dimension_size - len(result))) # fill the result with zeros + return result + + +def unpack_literal_tokens_to_array_dimensions(ID_token, array_dimensions, literal_tokens_list): + # gets array dimensions and list of (list of list of...) literal tokens to initialize it with + # returns one long list of literal tokens that can be used to initialize the array as a one dimensional array + # if there are missing literals to fill the entire array, then fill the blanks with NUM 0 + # E.g if the code is int arr[3][3][3] = {{1,2,3}, {}, {7, 8}} + # Then this function receives ([3,3,3] and [[1,2,3],[],[7,8]]) and returns [1,2,3,0,0,0,7,8,0] (all are tokens) + + array_size = dimensions_to_size(array_dimensions) # current size we need to fill + if all(not isinstance(element, list) for element in literal_tokens_list): + # special case - if all elements are literals, then we allow assigning them as-is and not care about dimensions + # E.g if we have arr[3][3][3] = {1,2,3,4} then return [1,2,3,4,0,0,0,0,0] + unpacked_literals_list = literal_tokens_list + [Token(Token.NUM, 0, 0, "0")] * ( + array_size - len(literal_tokens_list) + ) # fill missing with zeros + else: + unpacked_literals_list = unpack_multidimensional_literal_tokens_to_array_dimensions( + ID_token, array_dimensions, literal_tokens_list + ) + + if len(unpacked_literals_list) > array_size: + raise BFSemanticError( + "Tried to initialize array %s with incompatible amount of literals." + " (array size is %s and literals size is %s)" + % (ID_token, str(array_size), str(len(unpacked_literals_list))) + ) + assert len(unpacked_literals_list) == array_size + return unpacked_literals_list + + +def process_switch_cases(expression_code, cases): + # This function receives expression_code (string) and cases (list of tuples) corresponding to switch cases + # Each tuple is (case_value, case_code, has_break) + # And it returns code for the switch-case statement (string) + + if len(cases) == 0: + code = ">" # point to next cell + code += expression_code # evaluate expression + code += "<" # point to expression + code += "<" # discard result + return code + + def process_cases(cases): + # This function gets the cases list of tuples + # And returns 2 values: default_code (string), all_cases_have_break (bool) + # Note - default_code includes code of all relevant cases that are after the default case (if there's no break) + all_cases_have_break = all(has_break for (_, _, has_break) in cases) + + has_default, default_code = False, "" + for case, case_code, has_break in cases: + if case == "default": + has_default = True + if has_default: + default_code += case_code + if has_break: + break + return default_code, all_cases_have_break + + default_code, all_cases_have_break = process_cases(cases) + + # using 2 temp cells: need_to_execute, expression_value + # need_to_execute - initialized with 1, zeroed if running any case. indicating we should execute code for one of the cases + # expression_value - initialized with expression's value, this is what we compare our cases' values to + + code = "[-]+" # need_to_execute = 1 + code += ">" # point to next cell + code += expression_code # evaluate expression + code += "<" # point to expression + + if all_cases_have_break: # small optimization for evaluating the expression + cases = [ + case for case in cases if case[0] != "default" + ] # remove default to be able to sort. it is handled differently + cases.sort(key=lambda x: x[0], reverse=True) # Can sort since correct flow is not needed + + """ + This loop compares the expression value to each case in the switch-case statement, in reverse order + It does so by increasing and decreasing expression, and comparing result to 0 + E.G. if we have + switch(x) { + case 2: + case 0: + case 5: + case 1: + } + x will be put in cell, then: + Iteration 1 will "increase" cell by -1 (0-1) (comparing x with 1) + Iteration 2 will "increase" cell by -4 (1-5) (comparing x with 5) + Iteration 3 will increase cell by +5 (5-0) (comparing x with 0) + Iteration 4 will "increase" cell by -2 (0-2) (comparing x with 2) + """ + + # at this point, we point to expression_value cell + comparisons = 0 + last_case_val = 0 + for case, _, _ in reversed(cases): + if case == "default": + continue # default is handled differently + code += get_set_cell_value_code(-case, last_case_val) + last_case_val = -case + code += "[" # "if zero then jump to matching code part" + comparisons += 1 + + """ + Then we add each case's code in the correct order: + + [ + [ + [ + [ + + ] + ] + ] + ] + + notice each case uses the next case's ']' instruction to return to the comparisons block + for example, the '[' in case 5 line uses the ']' of case 1 code to "return" to the comparisons + this is because there is no way to "skip" code + """ + + # This code will execute after all the comparisons are done and non of the cases executed + if default_code: + code += ">" # point to next available cell for running the "default" code + code += default_code # add code for default case (it also includes all the following cases until break) + code += "<" # point to expression_value + code += "<-" # need_to_execute = 0 + code += ">[-]" # expression_value = 0. When going back to last comparison, it will be 0, so we skip the default + if comparisons > 0: + code += "]" # "jump back address" of the last comparison + comparisons -= 1 + + # Add all the cases code + for case_index, (case, case_code, has_break) in enumerate(cases): + if case == "default": + continue # default is handled differently + if has_break or case_code or default_code: # Meaning this case is not identical to the following case + # Or there exist a default case. And because it is handled differently, we need to have its code multiple times in different locations + # (if they are identical then no need to generate the same code multiple times (one for each case). + # this case will use the following case's code in the next loop iteration) + + # Generate code for this case (unique) + code += "<" # point to need_to_execute + code += "[" # if its non-zero (i.e need to execute the code for this case) + code += ">>" # point to next available cell for running the code + + # Insert the code from this case and all the following cases until reaching break + # This generates a lot of code since each case includes all following cases until reaching break + for _, following_case_code, following_has_break in cases[case_index:]: + code += following_case_code + if following_has_break: + break + code += "<<" # point to need_to_execute + code += "-" # need_to_execute=0 + code += "]" # # end if + code += ">" # point to expression_value + + if comparisons > 0: + code += "]" # "jump back address" of the comparison before us + comparisons -= 1 + + # end of the switch-case + code += "<" # point to need_to_execute, which becomes next available cell + return code + + +def get_copy_from_variable_code(ids_map_list, ID_token, current_pointer): + # returns code that copies the value from cell of variable ID to current pointer, and then sets the pointer to the next cell + + offset = get_offset_to_variable(ids_map_list, ID_token, current_pointer) + code = "[-]" # res = 0 + code += ">[-]" # temp (next cell) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + +def get_token_ID_code(ids_map_list, token, current_pointer): + # generate code that evaluates the ID token at the current pointer, and sets the pointer to point to the next available cell + return get_copy_from_variable_code(ids_map_list, token, current_pointer) + + +def get_literal_token_code(token): + # generate code that evaluates the token at the current pointer, and sets the pointer to point to the next available cell + assert is_token_literal(token) + if token.type == Token.TRUE: + code = "[-]" # zero current cell + code += "+" # current cell = 1 + code += ">" # point to next cell + return code + + elif token.type == Token.FALSE: + code = "[-]" # zero current cell + code += ">" # point to next cell + return code + + else: + value = get_literal_token_value(token) + code = "[-]" # zero current cell + code += get_set_cell_value_code(value, 0) # set current cell to the value + code += ">" # point to the next cell + return code + + +def get_divmod_code(right_token=None): + # given that the current pointer points to a, and the cell after a contains b, + # (i.e the cells look like: --> a, b, ?, ?, ?, ?, ...) + # returns a code that calculates divmod, and the cells look like this: + # --> 0, b-a%b, a%b, a/b, 0, 0 + # and the pointer points to the first 0 (which is in the same cell as a used to be) + ADD_DIVISION_BY_ZERO_CHECK = True + + if right_token is not None and right_token.type == Token.NUM: + if get_NUM_token_value(right_token) == 0: + raise BFSemanticError("Dividing by Zero, at %s" % right_token) + + ADD_DIVISION_BY_ZERO_CHECK = False + + def get_if_equal_to_0_code(inside_if_code, offset_to_temp_cell): + """ + given a , wraps it with an "if (current_cell == 0) {}" + + in the process, it zeros the current cell + additionally, it uses a temp cell + the argument is the offset from the current cell to the temp cell + *** note that the temp cell must be AFTER the cells that the touches *** + + should assume it starts running when pointing to the current cell + and it should end its run pointing to the same cell + """ + + # temp cell is initialized to 1, and holds a flag of whether or not we should run or not + # if cell to evaluate is not zero, we set this flag to 0 + + code = ">" * offset_to_temp_cell # point to temp + code += "[-]+" # temp = 1 + code += "<" * offset_to_temp_cell # point to cell to compare to 0 + + code += "[" # if it is not zero + code += ">" * offset_to_temp_cell # point to temp + code += "-" # temp = 0 + code += "<" * offset_to_temp_cell # point to cell + code += "[-]" # zero the cell + code += "]" # end if + + code += ">" * offset_to_temp_cell # point to temp cell + code += "[" # if it is non zero + code += "<" * offset_to_temp_cell # point to cell + code += inside_if_code # execute desired code + # at this point we point to the original cell + code += ">" * offset_to_temp_cell # point to temp cell + code += "-" # temp = 0 + code += "]" # end if + code += "<" * offset_to_temp_cell # point back to original cell + + return code + + code = "" + + if ADD_DIVISION_BY_ZERO_CHECK: + # create a prefix code: if (b == 0) {print("Error - Division by zero\n");} + + # copy b to temp cell (via another temp cell) and compare that cell to 0. if its 0, execute error print and go to infinite loop + + code += ">>" # point to empty cell + code += "[-]>[-]" # zero 2 temp cells + code += "<<" # point to b + code += "[>+>+<<-]" # move b to both cells + code += ">" # point to first cell + code += "[<+>-]" # move first cell back to b + code += ">" # point to second cell + + code_inside_if = get_print_string_code("Error - Division by zero\n") + code_inside_if += "[]" # infinite loop + + code += get_if_equal_to_0_code(code_inside_if, offset_to_temp_cell=1) + code += "<<<" # point to a + + # ======================= end of prefix ======================= + + # a, b, w, x, y, z + + code += ">>[-]>[-]>[-]>[-]<<<<<" # zero w,x,y,z, and point to a + code += "[" # while a != 0 + + code += "-" # decrease a by 1 + code += ">-" # decrease b by 1 + code += ">+" # increase w by 1 + code += "<" # point to b + code += "[->>>+>+<<<<]>>>>[-<<<<+>>>>]" # copy b to y (via z) + code += "<" # point to y + + code_inside_if = "" + code_inside_if += "<+" # increase x by 1 + code_inside_if += "<" # point to w + code_inside_if += "[-<+>]" # copy w to b (b is already 0) (after this we point to w) + code_inside_if += ">>" # point to y + + # get_if_equal_to_0 also zeros y + # i set offset_to_temp_cell = 1 because it can use z, since it is unused inside the if + code += get_if_equal_to_0_code(inside_if_code=code_inside_if, offset_to_temp_cell=1) + + code += "<<<<" # point to a + code += "]" # end while + + """ + a, b, w, x, y, z + + + w, x, y, z = 0, 0, 0, 0 + + while a != 0 + a -= 1 + b -= 1 + w += 1 + + if b == 0: (this means that w = original b) (implementation: copy b to y (via z) and compare y to 0, (then zero y)) + x += 1 + b = w + w = 0 + + at the end: + w = a%b + x = a/b + b = b-a%b + """ + + return code + + +def get_bitwise_code(code_logic): + # a, b, c, w, x, y, z, bit1, bitcounter, res + # code_logic uses the cells y, z, and bit1. Where y is res and z and bit1 are the bits. + # y is zero. z and bit1 should be zero after code_logic. + + code = ">" * 7 # point to bit1 + code += "[-]" # zero bit1 + code += ">" # point to bitcounter + code += ">[-]<" # zero res + + code += "[-]--------[++++++++" # while bitcounter != 8: + code += "<" + code += "<[-]" * 5 # clear c, w, x, y, z + code += "++" # c = 2 + code += "<<" # point to a + + code += "[" # while a != 0: + code += "-" # a -= 1 + code += ">>-" # c -= 1 + code += "[>+>>+<<<-]>[<+>-]" # copy c to y (using w) + code += ">>" # point to y + code += ">>+<<" # bit1 += 1 + + code += "-[" # if y != 1: + code += "<+" # x += 1 + code += "<<++" # c += 2 (c was 0) + code += ">" * 5 # point to bit1 + code += "--" # bit1 -= 2 (bit1 was 2) + code += "<<" # point to y + code += "+" # set y to 0 + code += "]" # end if + + code += "<<<<<" # point to a + code += "]" # end while + + code += ">>>>[<<<<+>>>>-]" # move x to a (x is a/2) + code += "<<[-]++" # c = 2 + code += "<" # point to b + + code += "[" # while b != 0: + code += "-" # b -= 1 + code += ">-" # c -= 1 + code += "[>+>>+<<<-]>[<+>-]" # copy c to y (using w) + code += ">>" # point to y + code += ">+<" # z += 1 + + code += "-[" # if y != 1: + code += ">--<" # z -= 2 (z was 2) + code += "<+" # x += 1 + code += "<<++" # c += 2 (c was 0) + code += ">>>" # point to y + code += "+" # set y to 0 + code += "]" + + code += "<<<<" # point to b + code += "]" # end while + + # w is a % 2 + # x is a / 2 + + code += ">>>[<<<+>>>-]" # move x to b + + code += ">>" # point to z + code += code_logic # pointer ends at bit1, z and bit1 should be 0 after code + + code += ">[<+<+>>-]<[>+<-]" # copy bit to z (using bit1) + + # y = y << z + code += "<" + code += "[" # while z != 0: + code += "<" # point to y + code += "[<+>-]" # copy y to x + code += "<[>++<-]" # copy x to y * 2 + code += ">>-" # z -= 1 + code += "]" + + code += "<" # point to y + code += "[>>>>+<<<<-]" # res += y + + code += ">>>" # point to bitcounter + code += "-" * 7 # loop if bitcounter != 7 + + code += "]" # end while + + code += ">[<<<<<<<<<+>>>>>>>>>-]" # move res to a + code += "<<<<<<<<" # point to b + + return code + + +def get_unary_prefix_op_code(token, offset_to_variable=None): + # returns code that: + # performs op on an operand that is at the current pointer + # the result is placed in the cell of the operand + # and the pointer points to the cell right after it (which becomes the next available cell) + + if token.type == Token.NOT: + # a temp + code = ">" # point to temp + code += "[-]+" # temp = 1 + code += "<" # point to a + code += "[" # if a is non-zero + code += ">-" # temp = 0 + code += "<[-]" # zero a + code += "]" # end if + + code += ">" # point to temp + code += "[" # if temp is non-zero + code += "<+" # a = 1 + code += ">-" # temp = 0 + code += "]" # end if + + return code + + elif token.type == Token.INCREMENT: + # returns code that copies the value from the variable's cell at the given offset, and adds 1 to both the copied and the original cell + assert offset_to_variable is not None + offset = offset_to_variable + + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "+" # increase destination by 1 + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + elif token.type == Token.DECREMENT: + # returns code that copies the value from the variable's cell at the given offset, and subtracts 1 from both the copied and the original cell + assert offset_to_variable is not None + offset = offset_to_variable + + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "-" # decrease destination by 1 + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + elif token.type == Token.UNARY_MULTIPLICATIVE: + # returns code that copies the value from the variable's cell at the given offset, modifies both the copied and the original cell depending on the op + assert offset_to_variable is not None + offset = offset_to_variable + + if token.data in ["**", "//"]: + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * offset # point to res + code += ">" # point to temp (**x, //x keep x the same) + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell + + return code + + elif token.data == "%%": + code = "[-]" # res = 0 + code += "<" * offset # point to destination cell + code += "[-]" # zero destination + code += ">" * offset # point to res + code += ">" # point the next available cell + # at this point we point to the next available cell + + return code + + else: + raise BFSyntaxError("Unexpected unary prefix %s" % str(token)) + + elif token.type == Token.BITWISE_NOT: + # a temp + code = "[>+<-]" # move a into temp + code += ">" # point to temp + code += "+[<->-]" # invert temp into a + + return code + + elif token.type == Token.BINOP: + assert token.data in ["+", "-"] + if token.data == "+": + # keep value as-is + return ">" + elif token.data == "-": + # a temp + code = ">[-]" # zero temp + code += "<" # point to a + code += "[->-<]" # sub a from temp + code += ">" # point to temp + code += "[<+>-]" # copy temp to a + return code + raise NotImplementedError + + +def get_unary_postfix_op_code(token, offset_to_variable): + # returns code that: + # performs op on operand that is at the current pointer + # the result is placed in the cell of the operand + # and the pointer points to the cell right after it (which becomes the next available cell) + + if token.type == Token.INCREMENT: + # returns code that copies the value from the variable's cell at the given offset, and adds 1 to the original cell + offset = offset_to_variable + + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + code += "+" # increase temp by 1 + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + elif token.type == Token.DECREMENT: + # returns code that copies the value from the variable's cell at the given offset, and subtracts 1 from the original cell + offset = offset_to_variable + + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + code += "-" # decrease temp by 1 + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + elif token.type == Token.UNARY_MULTIPLICATIVE: + # returns code that copies the value from the variable's cell at the given offset, and modifies the original cell depending on the operation + offset = offset_to_variable + + code = "[-]" # res = 0 + code += ">[-]" # temp (next pointer) = 0 + code += "<" * (offset + 1) # point to destination cell + code += "[" + ">" * offset + "+>+" + "<" * (offset + 1) + "-]" # increase res and temp, zero destination + code += ">" * (offset + 1) # point to temp + + if token.data in ["**", "//"]: + pass # x**, x// keeps x the same + elif token.data == "%%": + # at this point we zeroed x and we point to temp (next available cell) + return code # no need to copy anything back to destination - x%% modifies x to 0 + else: + raise BFSyntaxError("Unexpected unary postfix %s" % str(token)) + + code += "[" + "<" * (offset + 1) + "+" + ">" * (offset + 1) + "-]" # copy temp back to destination + # at this point we point to the next available cell, which is temp, which is now zero + + return code + + raise NotImplementedError + + +def get_op_between_literals_code(op_token, right_token=None): + # returns code that: + # performs op on 2 operands + # the first operand is at current pointer, and the second operand is at current pointer + 1 + # the code can destroy second operand, and everything after it + + # the result is placed in the cell of the first operand + # and the pointer points to the cell right after it (which becomes the next available cell) + + op = op_token.data + if op == "+" or op == "-": + code = ">[<" + op + ">-]" # increase/decrease the first operand and decrease the second operand + # the pointer now points to the next available cell, which is the second operand, which is 0 + + return code + + elif op == "*": + # a, b, temp1, temp2 + code = ">>[-]" # temp1 = 0 + code += ">[-]" # temp2 = 0 + code += "<<<" # point to first operand + code += "[>>>+<<<-]" # move first operand to temp2 + code += ">>>" # point to temp2 + + # do in a loop: as long as temp2 != 0 + code += "[" + + code += "<<" # point to second operand + code += "[<+>>+<-]" # add it to first operand and temp1 + code += ">" # point to temp1 + code += "[<+>-]" # move it to second operand + + # end loop + code += ">" # point back to temp2 + code += "-" # decrease temp2 + code += "]" + + code += "<<" # point back to next available cell (second operand) + return code + + elif op == "/": + code = get_divmod_code(right_token) + code += ">>>" # point to a/b + code += "[<<<+>>>-]" # copy a/b to current cell + code += "<<" # point to next available cell + + return code + + elif op == "%": + code = get_divmod_code(right_token) + code += ">>" # point to a%b + code += "[<<+>>-]" # copy a%b to current cell + code += "<" # point to next available cell + + return code + + # relops + elif op == "==": + # a, b + code = "[->-<]" # a = 0, b = b - a + code += "+" # a = 1. will hold the result. if a!=b, this is unchanged + code += ">" # point to b + code += "[" # if b == 0, enter the following code + code += "<->[-]" # a = 0, b=0 + code += "]" # end of "loop" + + return code + + elif op == "!=": + # a, b + code = "[->-<]" # a = 0, b = b - a + # a will hold the result. if a != b, this is unchanged + code += ">" # point to b + code += "[" # if b == 0, enter the following code + code += "<+>[-]" # a = 1, b=0 + code += "]" # end of "loop" + + return code + + elif op == ">": + # a, b, c, d + + code = ">>[-]" # c = 0 (will hold res) + code += ">[-]" # d = 0 + code += "<<<" # point to a + + code += "[" # while a != 0 + + code += ">>[-]" # c = 0 + code += "<" # point to b + code += "[>+>+<<-]>[<+>-]" # copy b to d (via c) + code += "+" # c = 1 (will hold res) + code += ">" # point to d + code += "[" # if d != 0 + code += "[-]" # d = 0 + code += "<-" # c = 0 + code += "<-" # b -= 1 + code += ">>" # point to d + code += "]" # end if + + code += "<<<" # point to a + code += "-" # a -= 1 + + code += "]" # end while + + # move c to a + code += ">>" # point to c + code += "[<<+>>-]" # move c to a + code += "<" # point to b (next available cell) + + """ + x > y? + + res = 0 + while x != 0: + res = 1 + if y != 0: + res = 0 + y -= 1 + + x -= 1 + """ + + return code + + elif op == "<": + # similar to > + + # a, b, c, d + + code = ">>[-]" # c = 0 (will hold res) + code += ">[-]" # d = 0 + code += "<<" # point to b + + code += "[" # while b != 0 + + code += ">[-]" # c = 0 + code += "<<" # point to a + code += "[>>+>+<<<-]>>[<<+>>-]" # copy a to d (via c) + code += "+" # c = 1 (will hold res) + code += ">" # point to d + code += "[" # if d != 0 + code += "[-]" # d = 0 + code += "<-" # c = 0 + code += "<<-" # a -= 1 + code += ">>>" # point to d + code += "]" # end if + + code += "<<" # point to b + code += "-" # b -= 1 + + code += "]" # end while + + # move c to a + code += "<" # point to a + code += "[-]" # a = 0 + code += ">>" # point to c + code += "[<<+>>-]" # move c to a + code += "<" # point to b (next available cell) + + """ + x < y? + + res = 0 + while y != 0: + res = 1 + if x != 0: + res = 0 + x -= 1 + + y -= 1 + """ + + return code + + elif op == "<=": + # a, b, c, d + + code = ">>[-]+" # c = 1 (will hold res) + code += ">[-]" # d = 0 + code += "<<<" # point to a + + code += "[" # while a != 0 + + code += ">>[-]" # c = 0 + code += "<" # point to b + code += "[>+>+<<-]>[<+>-]" # copy b to d (via c) + code += ">" # point to d + code += "[" # if d != 0 + code += "[-]" # d = 0 + code += "<+" # c = 1 + code += "<-" # b -= 1 + code += ">>" # point to d + code += "]" # end if + + code += "<<<" # point to a + code += "-" # a -= 1 + + code += "]" # end while + + # move c to a + code += ">>" # point to c + code += "[<<+>>-]" # move c to a + code += "<" # point to b (next available cell) + + """ + x <= y? + + res = 1 + while x != 0: + res = 0 + + if y != 0: + res = 1 + y -= 1 + + x -= 1 + """ + + return code + + elif op == ">=": + # similar to <= + + # a, b, c, d + + code = ">>[-]+" # c = 1 (will hold res) + code += ">[-]" # d = 0 + code += "<<" # point to b + + code += "[" # while b != 0 + + code += ">[-]" # c = 0 + code += "<<" # point to a + code += "[>>+>+<<<-]>>[<<+>>-]" # copy a to d (via c) + code += ">" # point to d + code += "[" # if d != 0 + code += "[-]" # d = 0 + code += "<+" # c = 1 + code += "<<-" # a -= 1 + code += ">>>" # point to d + code += "]" # end if + + code += "<<" # point to b + code += "-" # b -= 1 + + code += "]" # end while + + # move c to a + code += "<" # point to a + code += "[-]" # a = 0 + code += ">>" # point to c + code += "[<<+>>-]" # move c to a + code += "<" # point to b (next available cell) + + """ + x >= y? + + res = 1 + while y != 0: + res = 0 + + if x != 0: + res = 1 + x -= 1 + + y -= 1 + """ + + return code + + elif op == "<<": + # a, b, temp + + code = ">>[-]" # zero temp + code += "<" # point to b + + code += "[" # while b != 0 + code += "<" # point to a + code += "[>>+<<-]" # copy a to temp + code += ">>" # point to temp + code += "[<<++>>-]" # multiply temp by 2 and store result in a + code += "<-" # point to b and b -= 1 + code += "]" # end while + + return code + + elif op == ">>": + # a, b, c, x, y, z + + code = ">" # point to b + code += ">[-]" * 4 # clear 4 cells + code += "<" * 4 # point to b + + code += "[" # while b != 0 + code += ">++" # set c to 2 + code += "<<" # point to a + + code += "[" # while a != 0 + code += "-" # a -= 1 + code += ">>-" # c -= 1 + code += "[>>+>+<<<-]>>>[<<<+>>>-]" # copy c to y (via z) + code += "<" # point to y + + code += "-[" # if y == 0 + code += "<+" # x += 1 + code += "<++" # set c to 2 + code += ">>" + code += "+" # zero y + code += "]" # end if + + code += "<<<<" # point to a + code += "]" # end while + + code += ">>>" # point to x + code += "[<<<+>>>-]" # move x to a + code += "<[-]" # zero c + code += "<-" # b -= 1 + code += "]" # end while + + return code + + elif op_token.type == Token.BITWISE_AND: + code = get_bitwise_code("[->[-<<+>>]<]>[-]") + + return code + + elif op_token.type == Token.BITWISE_OR: + code = get_bitwise_code("[>+<-]>[[-]<<+>>]") + + return code + + elif op_token.type == Token.BITWISE_XOR: + code = get_bitwise_code("[>-<-]>[[-]<<+>>]") + + return code + + raise NotImplementedError + + +def get_op_boolean_operator_code(node, current_pointer): + # short-circuit evaluation of AND and OR + assert node.token.type in [Token.AND, Token.OR] + + if node.token.type == Token.AND: + # result, operand + code = "[-]" # zero result + code += ">" # point to next cell + code += node.left.get_code(current_pointer + 1) # evaluate first operand + code += "<" # point to first operand + code += "[" # if it is non-zero + + code += "[-]" # zero first operand + code += node.right.get_code(current_pointer + 1) # evaluate second operand + code += "<" # point to second operand + code += "[" # if it is non-zero + code += "<+>" # result = 1 + code += "[-]" # zero second operand + code += "]" # end if + + code += "]" # end if + # now we point to one after result (next available cell) + return code + + elif node.token.type == Token.OR: + # result, check_second_operand/second_operand, first_operand + code = "[-]" # zero result + code += ">" # point to check_second_operand + code += "[-]+" # check_second_operand = 1 + code += ">" # point to next cell + code += node.left.get_code(current_pointer + 2) # evaluate first operand + code += "<" # point to first operand + + code += "[" # if it is non-zero + code += "<<+" # result = 1 + code += ">-" # check_second_operand = 0 + code += ">[-]" # zero first operand + code += "]" # end if + + code += "<" # point to check_second_operand + code += "[" # if check_second_operand + code += node.right.get_code(current_pointer + 1) # evaluate second operand + code += "<" # point to second operand + code += "[" # if it is non-zero + code += "<+>" # result = 1 + code += "[-]" # zero second operand + code += "]" # end if + code += "]" # end if + + # now we point to one after result (next available cell) + return code + + raise NotImplementedError + + +def get_print_string_code(string): + code = "[-]" # zero the current cell + code += ">[-]" # zero the next cell (will be used for loop counts) + code += "<" # point to the original cell ("character" cell) + + prev_value = 0 + for i in range(len(string)): + current_value = ord(string[i]) + + code += get_set_cell_value_code(current_value, prev_value, zero_next_cell_if_necessary=False) + code += "." + + prev_value = current_value + + return code + + +def get_move_right_index_cells_code(current_pointer, node_index): + # used for arrays + # returns a code that evaluates the index, then moves the pointer right, amount of cells + # at the end of execution, the layout is: + # 0 index next_available_cell (point to next available cell) + + # index, steps_taken_counter + code = node_index.get_code(current_pointer) # index + code += "[-]" # counter = 0 + code += "<" # point to index + + code += "[" # while index != 0 + code += ">>" # point to new_counter (one after current counter) + code += "[-]+" # zero new_counter then add 1 to the new_counter + code += "<" # move to old counter + code += "[>+<-]" # add old counter to new counter + code += "<" # point to old index + code += "-" # sub 1 from old index + code += "[>+<-]" # move old index to new index + code += ">" # point to new index + code += "]" # end while + + # old_index=0 new_index res (pointing to old index) + code += ">>" # point to res + + return code + + +def get_move_left_index_cell_code(): + # used for arrays + # complement of "get_move_right_index_cells_code" + # assumes the layout is: + # value, index (pointing to index) + # moves cells left, and moving along with it + # in the end, point to one cell after (which becomes the next available cell) + + # layout: res, index (pointing to index) + code = "[" # while new_index != 0 + code += "<" # point to res + code += "[<+>-]" # move res to the left + code += ">" # point to new_index + code += "-" # sub 1 from index + code += "[<+>-]" # move new_index to left + code += "<" # point to new index + code += "]" # end while + + # now res is at the desired cell, and we point to the next available cell + + return code + + +# ================= +# General +# ================= + + +def get_literal_token_value(token): + # known at compilation time + assert is_token_literal(token) + if token.type == Token.NUM: + return get_NUM_token_value(token) + elif token.type == Token.TRUE: + return 1 + elif token.type == Token.FALSE: + return 0 + elif token.type == Token.CHAR: + return ord(token.data) + + +def get_NUM_token_value(token): + if token.data.startswith("0x"): + return int(token.data, 16) + elif token.data.startswith("0o"): + return int(token.data, 8) + elif token.data.startswith("0b"): + return int(token.data, 2) + else: + return int(token.data) + + +def get_variable_from_ID_token(ids_map_list, ID_token): + ID = ID_token.data + # given an id, goes through the ids map list and returns the index of the first ID it finds + for i in range(len(ids_map_list)): + ids_map = ids_map_list[i].IDs_dict + if ID in ids_map: + return ids_map[ID] + raise BFSemanticError("'%s' does not exist" % str(ID_token)) + + +def dimensions_to_size(dimensions): + return reduce(lambda x, y: x * y, dimensions) + + +def get_variable_dimensions_from_token(ids_map_list, ID_token): + variable = get_variable_from_ID_token(ids_map_list, ID_token) + return variable.dimensions + + +def get_id_index(ids_map_list, ID_token): + variable = get_variable_from_ID_token(ids_map_list, ID_token) + return variable.cell_index + + +def get_offset_to_variable(ids_map_list, ID_token, current_pointer): + offset = current_pointer - get_id_index(ids_map_list, ID_token) + return offset + + +def is_token_literal(token): + # token with value that is known at compilation time + return token.type in [Token.TRUE, Token.FALSE, Token.NUM, Token.CHAR] diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Globals.py b/reasoning_gym/code/contrib/bfit/Compiler/Globals.py new file mode 100644 index 00000000..0eaaac80 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Globals.py @@ -0,0 +1,83 @@ +from collections import namedtuple + +from .General import dimensions_to_size, get_NUM_token_value +from .Token import Token + +""" +This file holds the program's functions and global variables +(as global variables, hehe) +And related functions +""" + +global_variables = list() # Global list of global variables + + +# variables +def get_global_variables(): + return global_variables + + +def insert_global_variable(variable): + get_global_variables().append(variable) + + +def get_global_variables_size(): + return sum(get_variable_size(variable) for variable in get_global_variables()) + + +def create_variable(name, type, dimensions): + # return variable named tuple + variable = namedtuple("variable", ["name", "type", "size", "cell_index"]) + + variable.name = name + variable.type = type + variable.dimensions = dimensions # list of array dimensions sizes (for non-arrays it will be [1]) + variable.cell_index = None # will be updated when we insert this variable into an ids map + + return variable + + +def get_variable_size(variable): + # return total variable size + return dimensions_to_size(variable.dimensions) + + +def get_variable_dimensions(variable): + return variable.dimensions + + +def is_variable_array(variable): + return variable.dimensions != [1] + + +def create_variable_from_definition(parser, index=None, advance_tokens=False): + """ + processes the variable definition at index, and returns the variable named tuple + if index is None, then assumes we start at the current_token_index + if advance_tokens is True, then modifies current_token_index accordingly using parser.advance_token() + """ + + if index is None: + index = parser.current_token_index + + assert parser.tokens[index].type == Token.INT + + parser.check_next_token_is(Token.ID, starting_index=index) + ID = parser.tokens[index + 1].data + + if advance_tokens: + parser.advance_token(amount=2) # skip INT ID + + if parser.tokens[index + 2].type == Token.LBRACK: # array (support multi-dimensional arrays) + dimensions = [] # element[i] holds the size of dimension[i] + while parser.tokens[index + 2].type == Token.LBRACK: + parser.check_next_tokens_are([Token.LBRACK, Token.NUM, Token.RBRACK], starting_index=index + 1) + dimensions.append(get_NUM_token_value(parser.tokens[index + 3])) + + if advance_tokens: + parser.advance_token(amount=3) # skip LBRACK NUM RBRACK + index += 3 + else: + dimensions = [1] + + return create_variable(ID, Token.INT, dimensions) diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Lexical_analyzer.py b/reasoning_gym/code/contrib/bfit/Compiler/Lexical_analyzer.py new file mode 100644 index 00000000..091bbcf7 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Lexical_analyzer.py @@ -0,0 +1,233 @@ +import re + +from .Optimizer import optimize +from .Token import Token + + +class LexicalErrorException(Exception): + pass + + +def analyze(text): + """ + :returns list of tokens in the text + raises exception in case of lexical error + """ + + rules = [ + (r"\s+", Token.WHITESPACE), + ("void", Token.VOID), + ("int", Token.INT), + ("bool", Token.INT), # treat bool as int + ("char", Token.INT), # treat char as int + ("true", Token.TRUE), + ("false", Token.FALSE), + ("&&", Token.AND), + (r"\|\|", Token.OR), + (r"\!", Token.NOT), + ("return", Token.RETURN), + ("if", Token.IF), + ("else", Token.ELSE), + ("while", Token.WHILE), + ("for", Token.FOR), + ("do", Token.DO), + ("print", Token.PRINT), + ("switch", Token.SWITCH), + ("case", Token.CASE), + ("default", Token.DEFAULT), + ("break", Token.BREAK), + ("continue", Token.CONTINUE), # todo + (":", Token.COLON), + (";", Token.SEMICOLON), + (",", Token.COMMA), + (r"\(", Token.LPAREN), + (r"\)", Token.RPAREN), + (r"\{", Token.LBRACE), + (r"\}", Token.RBRACE), + (r"\[", Token.LBRACK), + (r"\]", Token.RBRACK), + (r"=|\+=|-=|\*=|/=|%=|<<=|>>=|&=|\|=|\^=", Token.ASSIGN), + (r"\?", Token.TERNARY), + (r"<=|>=|==|!=|<|>", Token.RELOP), + (r"\+\+", Token.INCREMENT), + ("--", Token.DECREMENT), + (r"\+|-|\*|/|%", Token.BINOP), + (r"\*\*|//|%%", Token.UNARY_MULTIPLICATIVE), + ("<<|>>", Token.BITWISE_SHIFT), + ("~", Token.BITWISE_NOT), + ("&", Token.BITWISE_AND), + (r"\|", Token.BITWISE_OR), + (r"\^", Token.BITWISE_XOR), + ("([a-zA-Z_][a-zA-Z0-9_]*)", Token.ID), + (r"(\d+)", Token.NUM), + (r"(0x[A-Fa-f\d]+)", Token.NUM), # hexadecimal number + ("(0o[0-7]+)", Token.NUM), # octal number + ("(0b[01]+)", Token.NUM), # binary number + (r'\"(\\\"|[^"])*"', Token.STRING), + (r"\'(\\\'|(\\)?[^\'])\'", Token.CHAR), + ("//.*(\\n|$)", Token.COMMENT), + (r"/\*[\s\S]*?\*/", Token.COMMENT), # multiline comments + (".", Token.UNIDENTIFIED), + ] + + rules = [(re.compile(r), t) for r, t in rules] + + tokens = [] + + # create a mapping of [line number] to [offset of that line from the beginning of the text] + newline = re.compile("\n") + lines = [0] + [m.end() for m in re.finditer(newline, text)] + + i = 0 + while i < len(text): + current_matches = [] + for regex, token_type in rules: + m = regex.match(text, i) + if m: + current_matches.append((m, token_type)) + + # pick the token that fits the longest match + # if tie - pick the one defined first in the rules list + longest_match, max_i, matched_token = None, i, None + for match, token_type in current_matches: + if match.end() > max_i: + longest_match, max_i, matched_token = match, match.end(), token_type + + # calculate line and column + line, column = None, None + for line_idx in range(len(lines) - 1): + if lines[line_idx] <= longest_match.start() < lines[line_idx + 1]: + line, column = line_idx + 1, (longest_match.start() - lines[line_idx]) + 1 # humans count from 1 :) + break + if not line: + line, column = len(lines), (longest_match.start() - lines[-1]) + 1 + + if matched_token in [Token.COMMENT, Token.WHITESPACE]: + pass # do nothing + elif matched_token == Token.UNIDENTIFIED: + raise LexicalErrorException("Unidentified Character '%s' (line %s column %s)" % (text[i], line, column)) + elif matched_token in [Token.STRING, Token.CHAR]: + # remove quotes at beginning and end, un-escape characters + tokens.append( + Token(matched_token, line, column, longest_match.group()[1:-1].encode("utf8").decode("unicode_escape")) + ) + elif matched_token in [ + Token.NUM, + Token.ID, + Token.BINOP, + Token.RELOP, + Token.ASSIGN, + Token.UNARY_MULTIPLICATIVE, + Token.BITWISE_SHIFT, + ]: + tokens.append(Token(matched_token, line, column, longest_match.group())) + else: + tokens.append(Token(matched_token, line, column)) + i = longest_match.end() + + return tokens + + +def tests(): + def test1(): + # test token priorities: INT should not be confused with ID even if ID contains "int" + text = "my international int ; int; pints; international;" + res = analyze(text) + + expected = [ + Token.ID, + Token.ID, + Token.INT, + Token.SEMICOLON, + Token.INT, + Token.SEMICOLON, + Token.ID, + Token.SEMICOLON, + Token.ID, + Token.SEMICOLON, + ] + assert len(res) == len(expected) and all(res[i].type == expected[i] for i in range(len(res))) + + def test2(): + text = "true !||!false falsek k||y+-a&&x" + res = analyze(text) + + expected = [ + Token.TRUE, + Token.NOT, + Token.OR, + Token.NOT, + Token.FALSE, + Token.ID, + Token.ID, + Token.OR, + Token.ID, + Token.BINOP, + Token.BINOP, + Token.ID, + Token.AND, + Token.ID, + ] + assert len(res) == len(expected) and all(res[i].type == expected[i] for i in range(len(res))) + + def test3(): + text = "1+2" + tokens = analyze(text) + expected = [Token.NUM, Token.BINOP, Token.NUM] + assert len(tokens) == len(expected) and all(tokens[i].type == expected[i] for i in range(len(tokens))) + optimize(tokens) + assert len(tokens) == 1 and tokens[0].type == Token.NUM and tokens[0].data == "3" + + text = "1+2+3" + tokens = analyze(text) + expected = [Token.NUM, Token.BINOP, Token.NUM, Token.BINOP, Token.NUM] + assert len(tokens) == len(expected) and all(tokens[i].type == expected[i] for i in range(len(tokens))) + optimize(tokens) + assert len(tokens) == 1 and tokens[0].type == Token.NUM and tokens[0].data == "6" + + # make sure it is not optimized to 9 (3*3) + text = "1+2*3" + tokens = analyze(text) + expected = [Token.NUM, Token.BINOP, Token.NUM, Token.BINOP, Token.NUM] + assert len(tokens) == len(expected) and all(tokens[i].type == expected[i] for i in range(len(tokens))) + optimize(tokens) + assert len(tokens) == 1 and tokens[0].type == Token.NUM and tokens[0].data == "7" + + # test all arithmetic operations + text = "(1+2*3/6)+(1%3)*(6-1)" + tokens = analyze(text) + expected = [ + Token.LPAREN, + Token.NUM, + Token.BINOP, + Token.NUM, + Token.BINOP, + Token.NUM, + Token.BINOP, + Token.NUM, + Token.RPAREN, + Token.BINOP, + Token.LPAREN, + Token.NUM, + Token.BINOP, + Token.NUM, + Token.RPAREN, + Token.BINOP, + Token.LPAREN, + Token.NUM, + Token.BINOP, + Token.NUM, + Token.RPAREN, + ] + assert len(tokens) == len(expected) and all(tokens[i].type == expected[i] for i in range(len(tokens))) + optimize(tokens) + assert tokens[1].data == "2" and tokens[5].data == "1" and tokens[9].data == "5" + + # todo find a better way to test? + test1() + test2() + test3() + + +if __name__ == "__main__": + tests() diff --git a/reasoning_gym/code/contrib/bfit/Compiler/LibraryFunctionCompiler.py b/reasoning_gym/code/contrib/bfit/Compiler/LibraryFunctionCompiler.py new file mode 100644 index 00000000..feb2497a --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/LibraryFunctionCompiler.py @@ -0,0 +1,129 @@ +from .Functions import insert_function_object +from .Token import Token + + +class LibraryFunctionCompiler: + def __init__(self, name, type, parameters, code): + self.name = name + self.type = type + self.parameters = parameters + self.code = code + + def get_code(self, current_stack_pointer): + return self.code + + +def get_readint_code(): + # res, tmp, input, loop + # tmp is used for multiplication + """ + res = 0 + loop = 1 + + while loop + loop = 0 + input = input() + if input != newline # todo add a eof check as well. run it in several interpreters to look for common ways for "end of number" input + loop = 1 + res *= 10 + char_to_digit(input) + """ + + code = "[-]" # clear res = 0 + code += ">[-]" # tmp = 0 + code += ">>[-]+" # loop = 1 + + code += "[" # while loop == 1 + code += "[-]" # loop = 0 + code += "<" # point to input + code += "," # input character + code += "----------" # sub 10 (check for newline) + + code += "[" # if input is not newline + code += ">" # point to loop + code += "+" # loop = 1 + + # multiply res by 10 and add the input digit + code += "<<<" # point to res + code += "[>+<-]" # move res to tmp + code += ">" # point to tmp + code += "[<++++++++++>-]" # res = tmp * 10, tmp = 0 + code += ">" # point to input + code += "-" * ( + 0x30 - 10 + ) # convert character to a digit by subtracting 0x30 from it (we already subtracted 10 before) + code += "[<<+>>-]" # res += input + code += "]" # end if + + code += ">" # point to loop + code += "]" # end while + + code += "<<<" # point to res + + return code + + +def get_printint_code(): + # return_cell value_to_print_cell + + code = ">" # point to value_to_print cell + code += ">[-]" * 8 + "<" * 8 # zero some cells + + code += ">++++++++++<" # div amount + code += "[->-[>+>>]>[+[<+>-]>+>>]<<<<<]" # value_to_print/10 + code += ">[-]" # zero d-n%d + code += ">>" # point to div result + + code += ">++++++++++<" # div amount + code += "[->-[>+>>]>[+[<+>-]>+>>]<<<<<]" # res/10 + code += ">[-]" # zero d-n%d + code += ">>" # point to div result + + code += "[" # if the first digit is not 0 + code += ">++++++[<++++++++>-]<." # add 48 to the first digit and print it + code += "<<" + code += "+>" # set is_over_100 to true + code += "+>" # add 1 to the second digit so it prints even when it's 0 + code += "[-]" # zero the first digit + code += "]" # end if + + code += "<" # point to the second digit + + code += "[" # if the second digit is not 0 + code += "<[>-<-]" # if is_over_100 is true then subtract 1 from the second digit + code += "++++++[>++++++++<-]>." # add 48 to the second digit and print it + code += "[-]" # zero the second digit + code += "]" # end if + + code += "<<" # point to the cell after the third digit + code += "++++++[<++++++++>-]<." # add 48 to the third digit and print it + code += "[-]" # zero the third digit + code += "<<" # point to value_to_print_cell which is 0 + + code += "<" # point to return_cell + return code + + +def get_readchar_code(): + # read input into "return value cell". no need to move the pointer + code = "," + return code + + +def get_printchar_code(): + # point to parameter, output it, and then point back to "return value cell" + code = ">.<" + return code + + +def insert_library_functions(): + readint = LibraryFunctionCompiler("readint", Token.INT, list(), get_readint_code()) + insert_function_object(readint) + + printint = LibraryFunctionCompiler("printint", Token.VOID, [Token.INT], get_printint_code()) + insert_function_object(printint) + + readchar = LibraryFunctionCompiler("readchar", Token.INT, list(), get_readchar_code()) + insert_function_object(readchar) + + printchar = LibraryFunctionCompiler("printchar", Token.VOID, [Token.INT], get_printchar_code()) + insert_function_object(printchar) diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Minify.py b/reasoning_gym/code/contrib/bfit/Compiler/Minify.py new file mode 100644 index 00000000..ea0411e3 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Minify.py @@ -0,0 +1,14 @@ +def minify(code): + old_code = "" + + while old_code != code: + old_code = code + + code = code.replace("><", "") + code = code.replace("<>", "") + code = code.replace("+-", "") + code = code.replace("-+", "") + + code = code.replace("][-]", "]") + + return code diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Node.py b/reasoning_gym/code/contrib/bfit/Compiler/Node.py new file mode 100644 index 00000000..581ace92 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Node.py @@ -0,0 +1,437 @@ +from .Exceptions import BFSemanticError +from .General import ( + get_copy_from_variable_code, + get_copy_to_variable_code, + get_literal_token_code, + get_move_left_index_cell_code, + get_move_right_index_cells_code, + get_offset_to_variable, + get_op_between_literals_code, + get_op_boolean_operator_code, + get_token_ID_code, + get_unary_postfix_op_code, + get_unary_prefix_op_code, + get_variable_dimensions_from_token, + is_token_literal, + unpack_literal_tokens_to_array_dimensions, +) +from .Token import Token + +""" +This file holds classes that are used to create the parse tree of expressions +Each class implements a get_code() function that receives a "stack pointer" and returns code that evaluates the expression +""" + + +class Node: + def __init__(self, ids_map_list): + # holds a copy of ids_map_list as it was when we parsed the expression + self.ids_map_list = ids_map_list[:] + + def assign_token_to_op_token(self, assign_token): + assert assign_token.data in ["+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "|=", "^="] + + assignment_map = { + "+=": Token(Token.BINOP, assign_token.line, assign_token.column, data="+"), + "-=": Token(Token.BINOP, assign_token.line, assign_token.column, data="-"), + "*=": Token(Token.BINOP, assign_token.line, assign_token.column, data="*"), + "/=": Token(Token.BINOP, assign_token.line, assign_token.column, data="/"), + "%=": Token(Token.BINOP, assign_token.line, assign_token.column, data="%"), + "<<=": Token(Token.BITWISE_SHIFT, assign_token.line, assign_token.column, data="<<"), + ">>=": Token(Token.BITWISE_SHIFT, assign_token.line, assign_token.column, data=">>"), + "&=": Token(Token.BITWISE_AND, assign_token.line, assign_token.column), + "|=": Token(Token.BITWISE_OR, assign_token.line, assign_token.column), + "^=": Token(Token.BITWISE_XOR, assign_token.line, assign_token.column), + } + + op_token = assignment_map[assign_token.data] + op_node = NodeToken(self.ids_map_list, token=op_token) + return op_node + + def get_code(self, *args, **kwargs): + pass + + +class NodeToken(Node): + def __init__(self, ids_map_list, left=None, token=None, right=None): + Node.__init__(self, ids_map_list) + self.left = left + self.right = right + self.token = token + + def get_code(self, current_pointer, *args, **kwargs): + # returns the code that evaluates the parse tree + + if is_token_literal(self.token) or self.token.type == Token.ID: + # its a literal (leaf) + assert self.left is None and self.right is None + if self.token.type == Token.ID: + return get_token_ID_code(self.ids_map_list, self.token, current_pointer) + else: + return get_literal_token_code(self.token) + + elif self.token.type in [ + Token.BINOP, + Token.RELOP, + Token.BITWISE_SHIFT, + Token.BITWISE_AND, + Token.BITWISE_OR, + Token.BITWISE_XOR, + ]: + code = self.left.get_code(current_pointer) + code += self.right.get_code(current_pointer + 1) + code += "<<" # point to the first operand + + right_token = None + if isinstance(self.right, NodeToken): + right_token = self.right.token + + code += get_op_between_literals_code(self.token, right_token) + return code + + elif self.token.type in [Token.AND, Token.OR]: # short-circuit evaluation treated differently + return get_op_boolean_operator_code(self, current_pointer) + + elif self.token.type == Token.ASSIGN: + assert self.left.token.type == Token.ID + + if self.token.data == "=": + # id = expression + code = self.right.get_code(current_pointer) + + # create code to copy from evaluated expression to ID's cell + code += "<" # point to evaluated expression cell + code += get_copy_to_variable_code(self.ids_map_list, self.left.token, current_pointer) + code += ">" # point to next available cell + + return code + + else: + assert self.token.data in ["+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "|=", "^="] + # id += expression + # create a node for id + expression + + op_node = self.assign_token_to_op_token(self.token) + op_node.left = self.left + op_node.right = self.right + + # create a node for id = id + expression + assign_token = Token(Token.ASSIGN, self.token.line, self.token.column, data="=") + assignment_node = NodeToken(self.ids_map_list, left=self.left, token=assign_token, right=op_node) + + return assignment_node.get_code(current_pointer) + + +class NodeTernary(Node): + def __init__(self, ids_map_list, condition, node_true, node_false): + # node_condition ? node_true : node_false; + Node.__init__(self, ids_map_list) + self.condition = condition + self.node_true = node_true + self.node_false = node_false + + def get_code(self, current_pointer, *args, **kwargs): + # cells layout: + # result, bool_evaluate_node_false, condition + code = ">" # point to bool_evaluate_node_false + code += "[-]+" # bool_evaluate_node_false=1 + code += ">" # point to condition + code += self.condition.get_code(current_pointer + 2) # evaluate condition + code += "<" # point to condition + + code += "[" # if condition is non-zero + code += "<<" # point to result + code += self.node_true.get_code(current_pointer) # evaluate node_true + # now we point to bool_evaluate_node_false + code += "[-]" # zero bool_evaluate_node_false + code += ">" # point to condition + code += "[-]" # zero condition + code += "]" # end if + + code += "<" # point to bool_evaluate_node_false + code += "[" # if bool_evaluate_node_false is 1 + code += "<" # point to result + code += self.node_false.get_code(current_pointer) # evaluate node_false + # now we point to bool_evaluate_node_false + code += "[-]" # zero bool_evaluate_node_false + code += "]" # end if + # now we point to one cell after result - next available cell + return code + + +class NodeUnaryPrefix(Node): + def __init__(self, ids_map_list, operation, literal): + Node.__init__(self, ids_map_list) + self.token_operation = operation + self.node_literal = literal + + def get_code(self, current_pointer, *args, **kwargs): + # unary prefix (!x or ++x or ~x or -x) + assert self.token_operation.type in [ + Token.NOT, + Token.INCREMENT, + Token.DECREMENT, + Token.UNARY_MULTIPLICATIVE, + Token.BITWISE_NOT, + Token.BINOP, + ] + + if self.token_operation.type in [Token.NOT, Token.BITWISE_NOT, Token.BINOP]: + code = self.node_literal.get_code(current_pointer) + code += "<" # point to operand + code += get_unary_prefix_op_code(self.token_operation) + + return code + else: + # its INCREMENT or DECREMENT + if isinstance(self.node_literal, NodeArrayGetElement): + token_id, index_node = self.node_literal.token_id, self.node_literal.node_expression + code = get_move_right_index_cells_code(current_pointer, index_node) + + offset_to_array = get_offset_to_variable(self.ids_map_list, token_id, current_pointer + 2) + # it is +2 because in "get_move_right_index_cells_code", we moved 2 extra cells to the right, for retrieving the value + + code += get_unary_prefix_op_code(self.token_operation, offset_to_array) + + code += "<" # point to res + code += "[<<+>>-]" # move res to old "index cell" + code += "<" # point to new index cell + + code += get_move_left_index_cell_code() + return code + + # the token to apply on must be an ID + if isinstance(self.node_literal, NodeToken) is False: + raise BFSemanticError( + "Prefix operator %s can only be applied to a variable" % str(self.token_operation) + ) + + if self.node_literal.token.type != Token.ID: + raise BFSemanticError( + "Prefix operator %s cannot be applied to %s, but only to a variable" + % (str(self.token_operation), str(self.node_literal.token)) + ) + + offset_to_ID = get_offset_to_variable(self.ids_map_list, self.node_literal.token, current_pointer) + return get_unary_prefix_op_code(self.token_operation, offset_to_ID) + + +class NodeUnaryPostfix(Node): + def __init__(self, ids_map_list, operation, literal): + Node.__init__(self, ids_map_list) + self.token_operation = operation + self.node_literal = literal + + def get_code(self, current_pointer, *args, **kwargs): + # its an unary postfix operation (x++) + assert self.token_operation.type in [Token.INCREMENT, Token.DECREMENT, Token.UNARY_MULTIPLICATIVE] + + if isinstance(self.node_literal, NodeArrayGetElement): + token_id, index_node = self.node_literal.token_id, self.node_literal.node_expression + code = get_move_right_index_cells_code(current_pointer, index_node) + + offset_to_array = get_offset_to_variable(self.ids_map_list, token_id, current_pointer + 2) + # it is +2 because in "get_move_right_index_cells_code", we moved 2 extra cells to the right, for retrieving the value + + code += get_unary_postfix_op_code(self.token_operation, offset_to_array) + + code += "<" # point to res + code += "[<<+>>-]" # move res to old "index cell" + code += "<" # point to new index cell + + code += get_move_left_index_cell_code() + return code + + # the token to apply on must be an ID + if isinstance(self.node_literal, NodeToken) is False: + raise BFSemanticError("Postfix operator %s can only be applied to a variable" % str(self.token_operation)) + + if self.node_literal.token.type != Token.ID: + raise BFSemanticError( + "Postfix operator %s cannot be applied to %s, but only to a variable" + % (str(self.token_operation), str(self.node_literal.token)) + ) + + offset_to_ID = get_offset_to_variable(self.ids_map_list, self.node_literal.token, current_pointer) + return get_unary_postfix_op_code(self.token_operation, offset_to_ID) + + +class NodeFunctionCall(Node): + def __init__(self, ids_map_list, function_to_call, parameters): + """ + receives a FunctionCompiler object + that implements get_code() which gets a stack pointer and returns code + receives a list of parameters - Node objects + each one gets a stack pointer and returns code that evaluates the parameter + """ + Node.__init__(self, ids_map_list) + self.function_to_call = function_to_call + self.parameters = parameters + + def get_code(self, current_pointer, *args, **kwargs): + code = "[-]>" # return_value_cell=0 + + # evaluate parameters from left to right, and put them on the "stack" in that order + # after each parameter code, the pointer points to the next available cell (one after the parameter) + for i, parameter in enumerate(self.parameters): + code += parameter.get_code( + current_pointer + 1 + i + ) # evaluate each parameter at its cell offset (starting at one after return_value_cell) + + # at this point we point to one after the last parameter + code += "<" * len(self.parameters) # point back to first parameter + code += "<" # point to return_value_cell + code += self.function_to_call.get_code( + current_stack_pointer=current_pointer + ) # after this we point to return value cell + code += ">" # point to next available cell (one after return value) + return code + + +class NodeArrayElement(Node): + def __init__(self, ids_map_list): + Node.__init__(self, ids_map_list) + + """ + the idea: + 1. evaluate index. it is known only in run time, so we need to perform a little trick + 2. move steps to the right, while counting how many steps we moved so far + hold an index, and a steps_counter, and move them to the right while decreasing index and increasing steps_counter + e.g: 4,0 --> 3,1 --> 2,2 --> 1,3 --> 0,4 + (move right until index is 0. counter will hold the old index) + this way we know we moved steps, and know how many steps to go back when we are done + 3. move steps left, to get/set the relevant array element + this offset is known at compilation time + """ + + +class NodeArrayGetElement(NodeArrayElement): + """ + class for getting element of a one-dimensional array + it receives an expression, indicating the required index + and returns a code that gets that element + """ + + def __init__(self, ids_map_list, token_id, node_expression): + Node.__init__(self, ids_map_list) + self.token_id = token_id + self.node_expression = node_expression + + def get_code(self, current_pointer, *args, **kwargs): + code = get_move_right_index_cells_code(current_pointer, self.node_expression) + code += get_copy_from_variable_code(self.ids_map_list, self.token_id, current_pointer + 2) + # it is +2 because in "get_move_right_index_cells_code", we moved 2 extra cells to the right, for retrieving the value + + code += "<" # point to res + code += "[<<+>>-]" # move res to old "index cell" + code += "<" # point to new index cell + + code += get_move_left_index_cell_code() + return code + + +class NodeArraySetElement(NodeArrayElement): + """ + class for setting element of a one-dimensional array + it receives: + 1. an expression, indicating the required index + 2. assignment operator (=|+=|-=|*=|/=|%=|<<=|>>=|&=|(|=)|^=) + 3. an expression, indicating the value to be used for the assignment + and returns a code that gets that element + """ + + def __init__(self, ids_map_list, token_id, node_expression_index, assign_token, node_expression_value): + Node.__init__(self, ids_map_list) + self.token_id = token_id + self.node_expression_index = node_expression_index + + if assign_token.data == "=": + # id[exp] = expression + + self.assign_token = assign_token + self.node_expression_value = node_expression_value + + else: + # id[exp] += expression + assert assign_token.data in ["+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "|=", "^="] + + self.assign_token = Token(Token.ASSIGN, assign_token.line, assign_token.column, data="=") + + # create a node for id[exp] + expression + op_node = self.assign_token_to_op_token(assign_token) + op_node.left = NodeArrayGetElement(self.ids_map_list, token_id, node_expression_index) + op_node.right = node_expression_value + + self.node_expression_value = op_node + + def get_code(self, current_pointer, *args, **kwargs): + # index, steps_taken_counter, value + + code = self.node_expression_index.get_code(current_pointer) + code += "[-]" # counter = 0 + code += ">" # point to value cell + code += self.node_expression_value.get_code(current_pointer + 2) + code += "<<<" # point to index + + code += "[" # while index != 0 + code += ">>>" # point to new_value (one after current value) + code += "[-]" # zero new_value + code += "<" # move to old value + code += "[>+<-]" # move old value to new counter + code += "<" # point to old counter + code += "+" # increase old counter + code += "[>+<-]" # move old counter to new counter + code += "<" # point to old index + code += "-" # decrease old index + code += "[>+<-]" # move old index to new index + code += ">" # point to new index + code += "]" # end while + + code += ">>" # point to value + code += get_copy_to_variable_code(self.ids_map_list, self.token_id, current_pointer + 2) + # it is +2 because we moved 2 extra cells to the right, for pointing to value + + # layout: 0, idx, value (pointing to value) + # create layout: value, idx + code += "[<<+>>-]" # move value to old "index" cell (which is now 0) + + # value, index (pointing to one after index) + code += "<" # point to index + code += "[" # while index != 0 + code += "<" # point to value + code += "[<+>-]" # move value to the left + code += ">" # point to index + code += "-" # sub 1 from index + code += "[<+>-]" # move index to left + code += "<" # point to index + code += "]" # end while + + # now value is at the desired cell, and we point to the next available cell + + return code + + +class NodeArrayAssignment(Node): + """ + Used for array assignment + E.g arr = = { 1, 2, 3... } + """ + + def __init__(self, ids_map_list, token_id, literal_tokens_list): + Node.__init__(self, ids_map_list) + self.token_id = token_id + self.literal_tokens_list = literal_tokens_list + + def get_code(self, current_pointer, *args, **kwargs): + array_dimensions = get_variable_dimensions_from_token(self.ids_map_list, self.token_id) + unpacked_literals_list = unpack_literal_tokens_to_array_dimensions( + self.token_id, array_dimensions, self.literal_tokens_list + ) + + offset = get_offset_to_variable(self.ids_map_list, self.token_id, current_pointer) + code = "<" * offset # point to first array element + for literal in unpacked_literals_list: + code += get_literal_token_code(literal) # evaluate this literal and point to next array element + code += ">" * (offset - len(unpacked_literals_list)) # move back to the original position + code += ">" # point to the next cell + return code diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Optimizer.py b/reasoning_gym/code/contrib/bfit/Compiler/Optimizer.py new file mode 100644 index 00000000..992bfe49 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Optimizer.py @@ -0,0 +1,106 @@ +from .General import get_NUM_token_value +from .Token import Token + +""" +This file holds functions that optimize code on syntax-level. For example: +The tokens corresponding to the code "3*5" will be replaced in-place by a token that represents "15" +""" + + +def optimize_once(tokens): + # performs one pass on the tokens and optimizes them in-place if possible + # optimization based on a list of rules + + def optimize_binop(tokens, start_index): + # optimize arithmetic operations. E.g replace 1+2 with 3 + + # need to be careful not to optimize (1+2*3) to (3*3) + if ( + tokens[start_index + 1].data in ["*", "/", "%"] + or (start_index + 3 >= len(tokens)) + or (tokens[start_index + 3].data not in ["*", "/", "%"]) + ): + num1, num2 = get_NUM_token_value(tokens[start_index]), get_NUM_token_value(tokens[start_index + 2]) + op = tokens[start_index + 1].data + if op == "+": + val = num1 + num2 + elif op == "-": + val = num1 - num2 + if val < 0: # cannot optimize negative values + return False + elif op == "*": + val = num1 * num2 + elif op in ["/", "%"]: + if num2 == 0: + print("WARNING (optimizer) - division by zero at %s" % str(tokens[start_index])) + return False + if op == "/": + val = num1 // num2 + else: + val = num1 % num2 + else: + raise NotImplementedError(op) + + # remove the 3 old tokens and replace them with new one + new_token = Token( + Token.NUM, + tokens[start_index].line, + tokens[start_index].column, + data=str(val), + original_tokens=tokens[start_index : start_index + 3], + ) + + for _ in range(3): + tokens.pop(start_index) + tokens.insert(start_index, new_token) + return True + + return False + + def optimize_printint(tokens, start_index): + # replace printint(50) with print("50") + # since printing strings compiles into less Brainfuck code than printing ints + if tokens[start_index].data == "printint": + tokens[start_index] = Token( + Token.PRINT, tokens[start_index].line, tokens[start_index].column, original_tokens=[tokens[start_index]] + ) + tokens[start_index + 2] = Token( + Token.STRING, + tokens[start_index].line, + tokens[start_index].column, + data=str(tokens[start_index + 2].data), + original_tokens=[tokens[start_index + 2]], + ) + return True + + return False + + rules = [ + ([Token.NUM, Token.BINOP, Token.NUM], optimize_binop), # arithmetic operations + ([Token.ID, Token.LPAREN, Token.NUM, Token.RPAREN], optimize_printint), # printint(50) to print("50") + ] + + # try to match one of the rules to the tokens in a "sliding window" style + i = 0 + while i < len(tokens): + optimized = False + for tokens_sequence, optimization_function in rules: + if i + len(tokens_sequence) <= len(tokens): + if all(tokens_sequence[n] == tokens[i + n].type for n in range(len(tokens_sequence))): + if optimization_function(tokens, i): + optimized = True + if optimized: + continue # don't increment i, try to optimize the same location again + i += 1 + + +def optimize(tokens): + # optimize tokens again and again until there is nothing left to optimize + prev_tokens = [token.type for token in tokens] + while True: + optimize_once(tokens) + print(".", end="") + current_tokens = [token.type for token in tokens] + if current_tokens == prev_tokens: + break + prev_tokens = current_tokens diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Parser.py b/reasoning_gym/code/contrib/bfit/Compiler/Parser.py new file mode 100644 index 00000000..900ae41d --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Parser.py @@ -0,0 +1,133 @@ +from .Exceptions import BFSemanticError, BFSyntaxError +from .General import is_token_literal +from .Token import Token + + +class Parser: + """ + Used to easily iterate tokens + """ + + def __init__(self, tokens): + self.tokens = tokens + self.current_token_index = 0 + + # parsing tokens + def current_token(self): + if self.current_token_index >= len(self.tokens): + return None + else: + return self.token_at_index(self.current_token_index) + + def advance_token(self, amount=1): + self.current_token_index += amount + + def advance_to_token_at_index(self, token_index): + self.current_token_index = token_index + + def token_at_index(self, index): + assert index < len(self.tokens) + return self.tokens[index] + + def next_token(self, next_amount=1): + return self.token_at_index(self.current_token_index + next_amount) + + def find_matching(self, starting_index=None): + """ + :return: the index of the token that matches the current token + :param starting_index (optional) - the index of the token we want to match + + for example, if current token is { + it returns the index of the matching } + """ + if starting_index is None: + starting_index = self.current_token_index + + tokens = self.tokens + token_to_match = tokens[starting_index] + if token_to_match.type == Token.LBRACE: + inc = Token.LBRACE + dec = Token.RBRACE + elif token_to_match.type == Token.LBRACK: + inc = Token.LBRACK + dec = Token.RBRACK + elif token_to_match.type == Token.LPAREN: + inc = Token.LPAREN + dec = Token.RPAREN + else: + raise BFSemanticError("No support for matching %s" % str(token_to_match)) + + i = starting_index + cnt = 0 + while i < len(tokens): + if tokens[i].type == inc: + cnt += 1 + elif tokens[i].type == dec: + cnt -= 1 + + if cnt == 0: + return i + + i += 1 + + raise BFSyntaxError("Did not find matching %s for %s" % (dec, str(token_to_match))) + + def check_next_tokens_are(self, tokens_list, starting_index=None): + if starting_index is None: + starting_index = self.current_token_index + + # used for "assertion" and print a nice message to the user + if starting_index + len(tokens_list) >= len(self.tokens): + raise BFSyntaxError("Expected %s after %s" % (str(tokens_list), str(self.tokens[starting_index]))) + for i in range(0, len(tokens_list)): + if self.tokens[starting_index + 1 + i].type != tokens_list[i]: + raise BFSyntaxError( + "Expected %s after %s" + % (str(tokens_list[i]), [str(t) for t in self.tokens[starting_index : starting_index + 1 + i]]) + ) + + def check_next_token_is(self, token, starting_index=None): + self.check_next_tokens_are([token], starting_index=starting_index) + + def check_current_tokens_are(self, tokens_list): + self.check_next_tokens_are(tokens_list, starting_index=self.current_token_index - 1) + + def check_current_token_is(self, token): + self.check_current_tokens_are([token]) + + def compile_array_initialization_list(self): + # {1, 2, 3, ...} or {array_initialization_list, array_initialization_list, array_initialization_list, ...} or string + # parses the definition and returns a list (of list of list ....) of literal tokens (NUM, CHAR, TRUE, FALSE) + + list_tokens = [] + + if self.current_token().type == Token.STRING: + string_token = self.current_token() + line, column = string_token.line, string_token.column + for char in string_token.data: + list_tokens.append(Token(Token.NUM, line, column, str(ord(char)))) + + self.advance_token() # point to after STRING + return list_tokens + + assert self.current_token().type == Token.LBRACE + self.advance_token() # skip to after LBRACE + + while is_token_literal(self.current_token()) or self.current_token().type == Token.LBRACE: + if self.current_token().type == Token.LBRACE: # list of (literals | list) + list_tokens.append(self.compile_array_initialization_list()) + else: # literal + list_tokens.append(self.current_token()) + self.advance_token() # skip literal + + if self.current_token().type not in [Token.COMMA, Token.RBRACE]: + raise BFSyntaxError("Unexpected %s (expected comma (,) or RBRACE (}))" % self.current_token()) + + if self.current_token().type == Token.COMMA: + self.advance_token() # skip comma + if self.current_token().type == Token.RBRACE: + break + + self.check_current_token_is(Token.RBRACE) + self.advance_token() # skip RBRACE + return list_tokens diff --git a/reasoning_gym/code/contrib/bfit/Compiler/Token.py b/reasoning_gym/code/contrib/bfit/Compiler/Token.py new file mode 100644 index 00000000..df43a6f5 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Compiler/Token.py @@ -0,0 +1,70 @@ +class Token: + + INT = "INT" + VOID = "VOID" + TRUE = "TRUE" + FALSE = "FALSE" + AND = "AND" + OR = "OR" + NOT = "NOT" + RETURN = "RETURN" + IF = "IF" + ELSE = "ELSE" + WHILE = "WHILE" + FOR = "FOR" + DO = "DO" + BREAK = "BREAK" + CONTINUE = "CONTINUE" + SWITCH = "SWITCH" + CASE = "CASE" + DEFAULT = "DEFAULT" + COLON = "COLON" + SEMICOLON = "SEMICOLON" + COMMA = "COMMA" + + LPAREN = "LPAREN" + RPAREN = "RPAREN" + LBRACE = "LBRACE" + RBRACE = "RBRACE" + LBRACK = "LBRACK" + RBRACK = "RBRACK" + + ASSIGN = "ASSIGN" + TERNARY = "TERNARY" + RELOP = "RELOP" + BINOP = "BINOP" + INCREMENT = "INCREMENT" + DECREMENT = "DECREMENT" + UNARY_MULTIPLICATIVE = "UNARY_MULTIPLICATIVE" + + BITWISE_SHIFT = "BITWISE_SHIFT" + BITWISE_NOT = "BITWISE_NOT" + BITWISE_AND = "BITWISE_AND" + BITWISE_OR = "BITWISE_OR" + BITWISE_XOR = "BITWISE_XOR" + + WHITESPACE = "WHITESPACE" + ID = "ID" + NUM = "NUM" + STRING = "STRING" + CHAR = "CHAR" + + PRINT = "PRINT" + COMMENT = "COMMENT" + UNIDENTIFIED = "UNIDENTIFIED" + + def __init__(self, type, line, column, data=None, original_tokens=None): + self.type = type + self.line = line + self.column = column + self.data = data + self.original_tokens = original_tokens + + def __str__(self): + result = self.type + if self.data: + result += " " + self.data + result += " (line %s column %s)" % (self.line, self.column) + if self.original_tokens: + result += " (original tokens: " + ", ".join([str(t) for t in self.original_tokens]) + ")" + return result diff --git a/reasoning_gym/code/contrib/bfit/Compiler/__init__.py b/reasoning_gym/code/contrib/bfit/Compiler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reasoning_gym/code/contrib/bfit/Interpreter.py b/reasoning_gym/code/contrib/bfit/Interpreter.py new file mode 100644 index 00000000..9281772b --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/Interpreter.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import argparse +import sys + + +def create_jumps_dictionary(program): + lbraces = list() + res = dict() + + for index, command in enumerate(program): + if command == "[": + lbraces.append(index) + elif command == "]": + if len(lbraces) == 0: + raise SyntaxError("Brainfuck: mismatched parentheses (at index: %s)" % index) + + lbrace_index = lbraces.pop() + res[lbrace_index] = index + res[index] = lbrace_index + + if len(lbraces) > 0: + raise SyntaxError("Brainfuck: mismatched parentheses (at indexes: %s)" % str(lbraces)) + return res + + +def brainfuck(program, bits=8): + + jumps = create_jumps_dictionary(program) + data = dict() + data_pointer = 0 + + instruction_pointer = 0 + + while instruction_pointer < len(program): + command = program[instruction_pointer] + + if command == ">": + data_pointer += 1 + elif command == "<": + data_pointer -= 1 + elif command == "+": + data[data_pointer] = data.get(data_pointer, 0) + 1 + if data[data_pointer] == 2**bits: + data[data_pointer] = 0 + elif command == "-": + data[data_pointer] = data.get(data_pointer, 0) - 1 + if data[data_pointer] == -1: + data[data_pointer] = 2**bits - 1 + elif command == ",": + data[data_pointer] = ord(sys.stdin.read(1)) % 256 + elif command == ".": + print(chr(data.get(data_pointer, 0)), end="", flush=True) + elif command == "[": + if data.get(data_pointer, 0) == 0: + instruction_pointer = jumps[instruction_pointer] + elif command == "]": + if data.get(data_pointer, 0) != 0: + instruction_pointer = jumps[instruction_pointer] + else: # everything else is comment + pass + + instruction_pointer += 1 + + if data_pointer != 0: + print( + "WARNING (interpreter) - at the end of the execution the data pointer is %s instead of 0 (possibly a compiler issue)" + % str(data_pointer) + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("filepath") + parser.add_argument("--bits", "-b", "--interpreter-bits", type=int, default=8, help="Amount of bits each cell uses") + + args = parser.parse_args() + with open(args.filepath, "r") as f: + code = f.read() + + brainfuck(code, args.bits) diff --git a/reasoning_gym/code/contrib/bfit/LICENSE.md b/reasoning_gym/code/contrib/bfit/LICENSE.md new file mode 100644 index 00000000..e1249203 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 elikaski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/reasoning_gym/code/contrib/bfit/README.md b/reasoning_gym/code/contrib/bfit/README.md new file mode 100644 index 00000000..2b503c05 --- /dev/null +++ b/reasoning_gym/code/contrib/bfit/README.md @@ -0,0 +1,100 @@ +BF-it logo + +# **BF-it** +A compiler from a C-like language to Brainfuck + +Always heard that Brainfuck is Turing complete but still had doubts about its computational power? +Those days are over + + +## What is this? +BF-it is a Compiler of a C-like language to Brainfuck, written in Python + +You give it C-like code, and it gives you Brainfuck code with the same functionality :) + + +## Why did you create it? +There are several reasons why I chose to create this + +1. I always heard that Brainfuck is Turing complete, and + therefore as strong as any other language + * I wanted to put that to the test, and find a concrete + proof that it is true +2. I find it beautiful how a seemingly bunch of nonsense + characters can have a logical meaning / functionality + * I wanted to take it to the extreme +3. I wanted to practice my developing skills +4. It seemed to be like a real challenge (and it was!) +5. But mostly, and most importantly, I did it for teh lulz + + +## What can I use it for? +Create a Brainfuck code of your choice, send it to your friends, and tell them to run it (on some online interpreter, for example) + +When they receive a bunch of nonsense which does something meaningful, they will (maybe) be amazed + + +## How do I use it? + +1. Write a C-like code (example programs included) into a file +2. Open your favorite command line interface +3. Run `BF-it.py ` + * This will compile your file and create a .bf file with + Brainfuck code + * (optional parameters: `-o` for output file, and `-r` + for running the compiled file) +4. Run `Interpreter.py `, this will execute + the Brainfuck code + +Example: +``` +$ cat helloworld.code +int main() +{ + print("Hello World!\n"); +} + +$ ./BF-it.py helloworld.code +Compiling file 'helloworld.code'... +Compiled successfully to 'helloworld.bf' +$ cat helloworld.bf +>[-]>[-]<>++++++++[-<+++++++++>]<.>++++[-<+++++++>] +<+.+++++++..+++.>++++++[-<------------->]<-.>+++++[ +-<+++++++++++>]<.>++++[-<++++++>]<.+++.------.----- +---.>++++++[-<----------->]<-.>+++[-<------->]<--.< + +$ ./Interpreter.py helloworld.bf +Hello World! +``` + +For a more interesting example, check out Tic-Tac-Toe! + +Tic-Tac-Toe
+ +1. Take a look at tic_tac_toe.code in the 'examples/games' directory +2. Run ```./BF-it.py examples/games/tic_tac_toe.code -r``` +3. Play around +4. If you're brave, take a look at the generated Brainfuck code +5. If you're very brave, try to understand it (I can't) + +## How do I write code? +Please refer to the 'examples' directory. +It contains examples and explanations for syntax, operations, flow, and more + + +## I would like to add a feature / fix a bug +If you would like to add a feature yourself, perform a pull request and add your changes. I will review them + +If you found a bug, or have an idea for a feature, open an issue + + + +## References I used: +* https://esolangs.org/wiki/brainfuck_algorithms for basic Brainfuck algorithms +* https://www.iamcal.com/misc/bf_debug/ for debugging Brainfuck code +* https://fatiherikli.github.io/brainfuck-visualizer/ for debugging Brainfuck code +* https://godbolt.org/ for comparing my C-like compiler to other C compilers' implementations +* https://www.lysator.liu.se/c/ANSI-C-grammar-y.html for creating a grammar +* https://introcs.cs.princeton.edu/java/11precedence/ for operator precedence +* https://logomakr.com/ for creating a logo +* https://www.youtube.com/ for setting the mood diff --git a/reasoning_gym/code/contrib/bfit/__init__.py b/reasoning_gym/code/contrib/bfit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reasoning_gym/cognition/__init__.py b/reasoning_gym/cognition/__init__.py index f5d43196..38baf31b 100644 --- a/reasoning_gym/cognition/__init__.py +++ b/reasoning_gym/cognition/__init__.py @@ -6,12 +6,21 @@ - Working memory """ +from .arc_1d import Arc1DConfig, Arc1DDataset from .color_cube_rotation import ColorCubeRotationConfig, ColorCubeRotationDataset +from .figlet_fonts import FigletFontConfig, FigletFontDataset from .number_sequences import NumberSequenceConfig, NumberSequenceDataset +from .rubiks_cube import RubiksCubeConfig, RubiksCubeDataset __all__ = [ - "NumberSequenceConfig", - "NumberSequenceDataset", + "Arc1DConfig", + "Arc1DDataset", "ColorCubeRotationConfig", "ColorCubeRotationDataset", + "FigletFontConfig", + "FigletFontDataset", + "NumberSequenceConfig", + "NumberSequenceDataset", + "RubiksCubeConfig", + "RubiksCubeDataset", ] diff --git a/reasoning_gym/cognition/arc_1d.py b/reasoning_gym/cognition/arc_1d.py new file mode 100644 index 00000000..7e399f20 --- /dev/null +++ b/reasoning_gym/cognition/arc_1d.py @@ -0,0 +1,112 @@ +from dataclasses import dataclass +from random import Random +from typing import Optional + +from ..dataset import ProceduralDataset +from ..factory import register_dataset + + +@dataclass +class Arc1DConfig: + """Configuration for ARC 1D task generation""" + + min_size: int = 10 # Minimum grid size + max_size: int = 30 # Maximum grid size + num_train: int = 3 # Number of training examples + seed: Optional[int] = None + size: int = 500 + + def validate(self) -> None: + """Validate configuration parameters""" + assert self.min_size > 0, "min_size must be positive" + assert self.max_size >= self.min_size, "max_size must be >= min_size" + assert self.num_train > 0, "num_train must be positive" + assert self.size > 0, "size must be positive" + + +class Arc1DDataset(ProceduralDataset): + """ + Generates ARC 1D tasks by randomly selecting from available task generators + + This dataset is a procedural variant of the 1D-ARC dataset which is described in the paper: + `LLMs and the Abstraction and Reasoning Corpus: Successes, Failures, and the Importance + of Object-based Representations` (https://arxiv.org/abs/2305.18354) + + Ilya Sheprut (optozorax) created rust generators for most of the ARC 1d tasks. For + reasoning-gym rust tasks were machine-converted to python via Sonnet. + + Ilya's original rust code can be found here: https://github.com/optozorax/arc_1d/ + """ + + def __init__(self, config: Arc1DConfig): + from .arc_1d_tasks import ARC_1D_TASKS + + super().__init__(config=config, seed=config.seed, size=config.size) + self.ARC_1D_TASKS = ARC_1D_TASKS + self.task_names = list(ARC_1D_TASKS.keys()) + + def __getitem__(self, idx: int) -> dict: + """Generate a single ARC 1D task with training examples + + Args: + idx: Index of the item to generate + + Returns: + dict with keys: + - question: str, the task description and examples + - answer: str, the expected output format + - metadata: dict with generation parameters + """ + # Create deterministic RNG from base seed and idx + item_rng = Random(self.seed + idx) + + # Select random task + task_name = item_rng.choice(self.task_names) + task_func, task_kwargs = self.ARC_1D_TASKS[task_name] + + # Generate training examples + train_examples = [] + size = item_rng.randint(self.config.min_size, self.config.max_size) + + for _ in range(self.config.num_train): + example = None + while example is None: + example = task_func(item_rng, size, **task_kwargs) + + train_examples.append(example) + + # Generate test example + test_example = None + while test_example is None: + test_example = task_func(item_rng, size, **task_kwargs) + + # Format question + question = "Find the common rule that maps an input grid to an output grid, given the examples below.\n\n" + + # Add training examples + for i, example in enumerate(train_examples, 1): + question += f"Example {i}:\n" + question += "Input: " + " ".join(str(x) for x in example["input"]) + "\n" + question += "Output: " + " ".join(str(x) for x in example["output"]) + "\n\n" + + # Add test input + question += "Below is a test input grid. Predict the corresponding output grid by applying the rule you found. " + question += "Describe how you derived the rule and your overall reasoning process in detail before you submit your answer. " + question += "Your final answer must be placed in tags and should be just be the text output grid itself.\n\n" + question += "Input:\n" + question += " ".join(str(x) for x in test_example["input"]) + + return { + "question": question, + "answer": " ".join(str(x) for x in test_example["output"]), + "metadata": { + "task_name": task_name, + "size": size, + "train_examples": train_examples, + "test_example": test_example, + }, + } + + +# Register the dataset +register_dataset("arc_1d", Arc1DDataset, Arc1DConfig) diff --git a/reasoning_gym/cognition/arc_1d_tasks.py b/reasoning_gym/cognition/arc_1d_tasks.py new file mode 100644 index 00000000..61151b34 --- /dev/null +++ b/reasoning_gym/cognition/arc_1d_tasks.py @@ -0,0 +1,1227 @@ +from random import Random +from typing import Dict, List, Optional + + +def gen_field(size: int, color: int = 0) -> List[int]: + """Generate a field of given size filled with specified color (default 0).""" + return [color] * size + + +def write_block(pos: int, block: List[int], field: List[int]) -> List[int]: + """Write a block into a field at given position.""" + result = field.copy() + for i, color in enumerate(block): + result[pos + i] = color + return result + + +def task_move_n_pix(rng: Random, size: int, move_pix: int, solid: bool) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block is moved to the right by move_pix pixels.""" + if size <= move_pix + 1: + return None + + block_size = rng.randint(1, size - move_pix - 1) + block_pos = rng.randint(0, size - block_size - move_pix) + + if solid: + color = rng.randint(1, 9) + block = [color] * block_size + else: + block = [rng.randint(1, 9) for _ in range(block_size)] + + question = write_block(block_pos, block, gen_field(size)) + answer = write_block(block_pos + move_pix, block, gen_field(size)) + + return {"input": question, "output": answer} + + +def task_move_n_pix_wrapped(rng: Random, size: int, move_pix: int, solid: bool) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block is moved to the right by move_pix pixels with wrapping.""" + block_size = rng.randint(1, size) + block_pos = rng.randint(0, size) + + if solid: + color = rng.randint(1, 9) + block = [color] * block_size + else: + block = [rng.randint(1, 9) for _ in range(block_size)] + + question = gen_field(size) + answer = gen_field(size) + + for i, color in enumerate(block): + question[(block_pos + i) % size] = color + answer[(block_pos + move_pix + i) % size] = color + + return {"input": question, "output": answer} + + +def task_gravity(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where all non-zero elements are attracted to the left.""" + density = 0.5 + question = [rng.randint(1, 9) if rng.random() < density else 0 for _ in range(size)] + + non_zero = [x for x in question if x != 0] + answer = non_zero + [0] * (size - len(non_zero)) + + return {"input": question, "output": answer} + + +def task_gravity_counting(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where non-zero elements are counted and represented as a sequence of 1s.""" + density = 0.5 + question = [rng.randint(1, 9) if rng.random() < density else 0 for _ in range(size)] + + count = sum(1 for x in question if x != 0) + answer = [1] * count + [0] * (size - count) + + return {"input": question, "output": answer} + + +def task_gravity_antigravity(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where color 1 moves right and color 2 moves left.""" + density = 0.5 + question = [rng.randint(1, 2) if rng.random() < density else 0 for _ in range(size)] + + color1 = [x for x in question if x == 1] + color2 = [x for x in question if x == 2] + answer = [2] * len(color2) + [0] * (size - len(color1) - len(color2)) + [1] * len(color1) + + return {"input": question, "output": answer} + + +def task_block_touch_dot(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block moves to touch (but not cover) a dot.""" + dot_color = 1 + block_color = rng.randint(2, 9) + + block_size = rng.randint(1, size) + dot_pos = rng.randint(0, size) + + can_place_left = dot_pos >= block_size + can_place_right = dot_pos + block_size < size + + if not (can_place_left or can_place_right): + return None + + if can_place_left and can_place_right: + side = rng.choice(["left", "right"]) + elif can_place_left: + side = "left" + else: + side = "right" + + if side == "left": + q_block_pos = rng.randint(0, dot_pos - block_size) + a_block_pos = dot_pos - block_size + else: + q_block_pos = rng.randint(dot_pos + 1, size - block_size) + a_block_pos = dot_pos + 1 + + question = gen_field(size) + question[dot_pos] = dot_color + question = write_block(q_block_pos, [block_color] * block_size, question) + + answer = gen_field(size) + answer[dot_pos] = dot_color + answer = write_block(a_block_pos, [block_color] * block_size, answer) + + return {"input": question, "output": answer} + + +def task_block_touch_dot_n_pix(rng: Random, size: int, move_pix: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block moves move_pix pixels toward a dot.""" + dot_color = 2 + block_color = rng.randint(3, 9) + + block_size = rng.randint(1, size) + dot_pos = rng.randint(0, size) + + can_place_left = dot_pos >= block_size + can_place_right = dot_pos + block_size < size + + if not (can_place_left or can_place_right): + return None + + if can_place_left and can_place_right: + side = rng.choice(["left", "right"]) + elif can_place_left: + side = "left" + else: + side = "right" + + if side == "left": + q_block_pos = rng.randint(0, dot_pos - block_size) + distance = (dot_pos - block_size) - q_block_pos + move = min(distance, move_pix) + a_block_pos = q_block_pos + move + else: + q_block_pos = rng.randint(dot_pos + 1, size - block_size) + distance = q_block_pos - (dot_pos + 1) + move = min(distance, move_pix) + a_block_pos = q_block_pos - move + + question = gen_field(size) + question[dot_pos] = dot_color + question = write_block(q_block_pos, [block_color] * block_size, question) + + answer = gen_field(size) + answer[dot_pos] = dot_color + answer = write_block(a_block_pos, [block_color] * block_size, answer) + + return {"input": question, "output": answer} + + +def task_block_scale_to_dot(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block scales to touch a dot (keeping one end fixed).""" + dot_color = 2 + block_color = rng.randint(3, 9) + + block_size = rng.randint(1, size) + dot_pos = rng.randint(0, size) + + can_place_left = dot_pos >= block_size + can_place_right = dot_pos + block_size < size + + if not (can_place_left or can_place_right): + return None + + if can_place_left and can_place_right: + side = rng.choice(["left", "right"]) + elif can_place_left: + side = "left" + else: + side = "right" + + if side == "left": + q_block_pos = rng.randint(0, dot_pos - block_size) + new_size = dot_pos - q_block_pos + 1 + a_block_pos = q_block_pos + else: + q_block_pos = rng.randint(dot_pos + 1, size - block_size) + new_size = (q_block_pos + block_size) - dot_pos + a_block_pos = dot_pos + + question = gen_field(size) + question[dot_pos] = dot_color + question = write_block(q_block_pos, [block_color] * block_size, question) + + answer = gen_field(size) + answer[dot_pos] = dot_color + answer = write_block(a_block_pos, [block_color] * new_size, answer) + + return {"input": question, "output": answer} + + +def task_two_points_and_fill(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where space between two points of same color is filled with that color.""" + color = rng.randint(1, 9) + + pos1 = rng.randint(0, size - 1) + pos2 = rng.randint(0, size - 1) + if pos1 == pos2: + return None + + pos1, pos2 = min(pos1, pos2), max(pos1, pos2) + + question = gen_field(size) + question[pos1] = color + question[pos2] = color + + answer = question.copy() + for i in range(pos1, pos2 + 1): + answer[i] = color + + return {"input": question, "output": answer} + + +def task_reflect_block_with_border_pixel(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block with a border pixel is reflected.""" + block_size = rng.randint(2, size) + if block_size > size: + return None + + c1 = rng.randint(1, 9) + c2 = rng.randint(1, 9) + if c1 == c2: + return None + + side = "left" if rng.random() < 0.5 else "right" + pos = rng.randint(0, size - block_size) + + block = [c1] * block_size + if side == "left": + block[0] = c2 + else: + block[block_size - 1] = c2 + + question = write_block(pos, block, gen_field(size)) + reversed_block = block[::-1] # Reverse the block + answer = write_block(pos, reversed_block, gen_field(size)) + + return {"input": question, "output": answer} + + +def task_reflect_block_with_border_pixel_random(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a random-colored block with a border pixel is reflected.""" + block_size = rng.randint(2, size) + if block_size > size: + return None + + side = "left" if rng.random() < 0.5 else "right" + pos = rng.randint(0, size - block_size) + + block = [rng.randint(1, 9) for _ in range(block_size)] + border_color = rng.randint(1, 9) + + if side == "left": + if block[0] == border_color: + return None + block[0] = border_color + else: + if block[block_size - 1] == border_color: + return None + block[block_size - 1] = border_color + + question = write_block(pos, block, gen_field(size)) + reversed_block = block[::-1] # Reverse the block + answer = write_block(pos, reversed_block, gen_field(size)) + + return {"input": question, "output": answer} + + +def task_reflect_block_around_dot(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block is reflected around a dot.""" + dot_color = 2 + + dot_pos = rng.randint(0, size) + block_size = rng.randint(1, size) + block_pos = rng.randint(0, size - block_size) + block_end = block_pos + block_size - 1 + + # Check if block is strictly to left or right of dot + strictly_left = block_end < dot_pos + strictly_right = block_pos > dot_pos + + if not (strictly_left or strictly_right): + return None + + block_color = rng.randint(3, 9) # Different from dot color + block = [block_color] * block_size + + # Calculate reflection bounds + min_reflect = 2 * dot_pos - block_end + max_reflect = 2 * dot_pos - block_pos + if min_reflect < 0 or max_reflect >= size: + return None + + question = gen_field(size) + question = write_block(block_pos, block, question) + question[dot_pos] = dot_color + + answer = gen_field(size) + answer[dot_pos] = dot_color + for i in range(block_size): + reflect_idx = 2 * dot_pos - (block_pos + i) + answer[reflect_idx] = block[i] + + return {"input": question, "output": answer} + + +def task_block_and_noise_remove(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where noise around a block needs to be removed.""" + block_size = rng.randint(2, size) + if block_size > size: + return None + + block_pos = rng.randint(0, size - block_size) + color = rng.randint(1, 9) + + # Create field with block + field = gen_field(size) + for i in range(block_size): + field[block_pos + i] = color + + # Track forbidden positions for noise + forbidden = [False] * size + for i in range(block_pos, block_pos + block_size): + forbidden[i] = True + if block_pos > 0: + forbidden[block_pos - 1] = True + if block_pos + block_size < size: + forbidden[block_pos + block_size] = True + + # Add noise + noise_count = rng.randint(1, 3) + noise_positions = [] + + for _ in range(noise_count): + allowed = [i for i in range(size) if not forbidden[i]] + if not allowed: + break + noise_pos = rng.choice(allowed) + noise_positions.append(noise_pos) + field[noise_pos] = color + forbidden[noise_pos] = True + if noise_pos > 0: + forbidden[noise_pos - 1] = True + if noise_pos + 1 < size: + forbidden[noise_pos + 1] = True + + if len(noise_positions) < noise_count: + return None + + question = field + answer = field.copy() + for pos in noise_positions: + answer[pos] = 0 + + return {"input": question, "output": answer} + + +def task_block_and_noise_remove_inside(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where noise inside a block needs to be removed.""" + if size <= 6: + return None + + block_size = rng.randint(6, size) + if block_size > size: + return None + + block_pos = rng.randint(0, size - block_size) + color = rng.randint(1, 9) + + # Create field with block + field = gen_field(size) + for i in range(block_size): + field[block_pos + i] = color + + # Add noise inside block + max_noise = max(1, (block_size // 2) - 1) + noise_count = rng.randint(1, max_noise) + + positions = list(range(block_size)) + rng.shuffle(positions) + noise_positions = positions[:noise_count] + + for offset in noise_positions: + pos = block_pos + offset + noise_color = rng.randint(1, 9) + while noise_color == color: + noise_color = rng.randint(1, 9) + field[pos] = noise_color + + question = field + answer = field.copy() + for offset in noise_positions: + answer[block_pos + offset] = color + + return {"input": question, "output": answer} + + +def task_copy_block_to_dots(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block pattern is copied to dot positions.""" + block_size = 3 if rng.random() < 0.5 else 5 + if block_size >= size: + return None + + color = rng.randint(1, 9) + block = [color] * block_size + + # Generate dots with minimum distance to prevent overlap + min_gap = block_size + dot_positions = [] + pos = block_size + block_size // 2 + 1 + + while pos <= size - block_size: + if rng.random() < 0.5: # Control dot density + dot_positions.append(pos) + pos += min_gap + pos += 1 + + if not dot_positions: + return None + + question = gen_field(size) + question = write_block(0, block, question) + for pos in dot_positions: + question[pos] = color + + answer = gen_field(size) + answer = write_block(0, block, answer) + for pos in dot_positions: + block_start = pos - block_size // 2 + answer = write_block(block_start, block, answer) + + return {"input": question, "output": answer} + + +def task_copy_block_to_dots_colors(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block pattern is copied to dot positions with matching colors.""" + block_size = 3 if rng.random() < 0.5 else 5 + if block_size >= size: + return None + + block_color = rng.randint(1, 9) + block = [block_color] * block_size + + # Generate dots with minimum distance to prevent overlap + min_gap = block_size + dot_positions = [] + dot_colors = [] + pos = block_size + block_size // 2 + 1 + + while pos < size - block_size: + if rng.random() < 0.5: + dot_color = rng.randint(1, 9) + dot_positions.append(pos) + dot_colors.append(dot_color) + pos += min_gap + pos += 1 + + if not dot_positions: + return None + + question = gen_field(size) + question = write_block(0, block, question) + for i, pos in enumerate(dot_positions): + question[pos] = dot_colors[i] + + answer = gen_field(size) + answer = write_block(0, block, answer) + for i, pos in enumerate(dot_positions): + block_start = pos - block_size // 2 + colored_block = [dot_colors[i]] * block_size + answer = write_block(block_start, colored_block, answer) + + return {"input": question, "output": answer} + + +def task_paint_biggest_block(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where the largest block is painted a different color.""" + target_color = 1 + initial_color = rng.randint(2, 9) + + # Generate random blocks + question = gen_field(size) + blocks = [] + pos = 0 + + while pos < size: + if rng.random() < 0.4 and size - pos >= 2: + block_size = rng.randint(2, min(size - pos, 6)) + blocks.append((pos, block_size)) + for i in range(block_size): + question[pos + i] = initial_color + pos += block_size + 1 + else: + pos += 1 + + if len(blocks) < 2: + return None + + # Find biggest block + biggest_pos, biggest_size = max(blocks, key=lambda x: x[1]) + + # Check if there are multiple blocks of the same size + biggest_count = sum(1 for _, size in blocks if size == biggest_size) + if biggest_count > 1: + return None + + answer = question.copy() + for i in range(biggest_size): + answer[biggest_pos + i] = target_color + + return {"input": question, "output": answer} + + +def task_sort_blocks_by_size(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where blocks are sorted by size with 1 pixel gaps.""" + color = rng.randint(1, 9) + blocks = [] + pos = 0 + + # Generate random blocks with random sizes + while pos < size: + if rng.random() < 0.4 and size - pos >= 2: + block_size = rng.randint(1, min(size - pos, 6)) + blocks.append((pos, block_size)) + pos += block_size + rng.randint(1, 4) # Random gaps + else: + pos += 1 + + if len(blocks) < 2: + return None + + # Create input field + question = gen_field(size) + for pos, block_size in blocks: + for i in range(block_size): + question[pos + i] = color + + # Sort blocks by size + blocks.sort(key=lambda x: x[1]) + + # Check if sorted blocks fit with gaps + total_space = sum(size for _, size in blocks) + len(blocks) - 1 + if total_space > size: + return None + + # Create answer field with sorted blocks + answer = gen_field(size) + current_pos = 0 + + for _, block_size in blocks: + for i in range(block_size): + answer[current_pos + i] = color + current_pos += block_size + 1 # One pixel gap + + return {"input": question, "output": answer} + + +def task_sort_complete_sequence(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a complete sequence of block sizes is sorted.""" + # Calculate max possible block size given total array size + max_size = 1 + total_space = 0 + while total_space + max_size + 1 <= size: + total_space += max_size + 1 + max_size += 1 + max_size -= 1 + + if max_size < 2: + return None + + color = rng.randint(1, 9) + + # Create sequence of all sizes from 1 to max_size + blocks = list(range(1, max_size + 1)) + rng.shuffle(blocks) + + # Create input field with shuffled blocks + question = gen_field(size) + pos = 0 + for block_size in blocks: + for i in range(block_size): + question[pos + i] = color + pos += block_size + 1 + + # Create answer field with sorted blocks + answer = gen_field(size) + pos = 0 + for block_size in range(1, max_size + 1): + for i in range(block_size): + answer[pos + i] = color + pos += block_size + 1 + + return {"input": question, "output": answer} + + +def task_recolor_blocks_by_size(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where two blocks are recolored based on their size.""" + # Generate two different random sizes + size1 = rng.randint(2, 8) + size2 = rng.randint(2, 8) + while size2 == size1: + size2 = rng.randint(2, 8) + + # Ensure both blocks fit with at least 1 gap + if size1 + size2 + 1 > size: + return None + + # Place blocks with gap + pos1 = rng.randint(0, size - (size1 + size2 + 1)) + pos2 = rng.randint(pos1 + size1 + 1, size - size2) + + # Create input field with both blocks color 3 + question = gen_field(size) + for i in range(size1): + question[pos1 + i] = 3 + for i in range(size2): + question[pos2 + i] = 3 + + # Create answer field with recolored blocks + answer = question.copy() + if size1 > size2: + for i in range(size1): + answer[pos1 + i] = 1 + for i in range(size2): + answer[pos2 + i] = 2 + else: + for i in range(size1): + answer[pos1 + i] = 2 + for i in range(size2): + answer[pos2 + i] = 1 + + return {"input": question, "output": answer} + + +def task_gravity_one_step(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where non-zero elements move one step left if possible.""" + question = [rng.randint(1, 9) if rng.random() < 0.5 else 0 for _ in range(size)] + answer = question.copy() + + # Move each non-zero pixel one step left if possible + for i in range(1, size): + if answer[i] != 0 and answer[i - 1] == 0: + answer[i - 1] = answer[i] + answer[i] = 0 + + return {"input": question, "output": answer} + + +def task_move_block_by_own_size(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block moves right by its own size.""" + block_size = rng.randint(1, size // 2) # Ensure space for movement + pos = rng.randint(0, size - block_size * 2) # Space for block and movement + color = rng.randint(1, 9) + + question = gen_field(size) + block = [color] * block_size + question = write_block(pos, block, question) + + answer = write_block(pos + block_size, block, gen_field(size)) + + return {"input": question, "output": answer} + + +def task_change_to_five(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where all non-zero colors change to 5.""" + density = 0.5 + question = [rng.randint(1, 9) if rng.random() < density else 0 for _ in range(size)] + answer = [5 if x != 0 else 0 for x in question] + + return {"input": question, "output": answer} + + +def task_recolor_blocks_from_palette(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where blocks are recolored using a color palette.""" + # Generate blocks of same size + block_size = rng.randint(2, 4) + blocks = [] + pos = 0 + + while pos + block_size <= size: + if rng.random() < 0.4: + blocks.append(pos) + pos += block_size + 1 + else: + pos += 1 + + # Ensure we have space for palette + while blocks and blocks[-1] + block_size + len(blocks) + 1 >= size: + blocks.pop() + + if not blocks: + return None + + # Shift blocks right to make room for palette + palette_size = len(blocks) + blocks = [pos + palette_size + 1 for pos in blocks] + + # Generate color palette + colors = [] + for _ in range(len(blocks)): + while True: + color = rng.randint(1, 9) + if color not in colors: + colors.append(color) + break + + # Create question with color palette and blocks + question = gen_field(size) + + # Place color palette at start + for i, color in enumerate(colors): + question[i] = color + + # Place blocks of color 5 + for block_pos in blocks: + for i in range(block_size): + question[block_pos + i] = 5 + + # Create answer with recolored blocks + answer = question.copy() + for block_idx, block_pos in enumerate(blocks): + color = colors[block_idx] + for i in range(block_size): + answer[block_pos + i] = color + + return {"input": question, "output": answer} + + +def task_duplicate_block_from_seeds(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a block is duplicated from seed pixels.""" + block_size = rng.randint(2, 4) + if block_size + 1 >= size: + return None + if size <= 3 + block_size: + return None + + # Position block with space for seeds + block_pos = rng.randint(2, size - block_size - 1) + + # Decide seed placement + left_seed = rng.random() < 0.5 + right_seed = rng.random() < 0.5 + if not (left_seed or right_seed): + return None + + # Create input + question = gen_field(size) + + # Place main block + for i in range(block_size): + question[block_pos + i] = 1 + + # Place seeds with gaps + seeds = [] + if left_seed: + color = rng.randint(1, 9) + question[block_pos - 2] = color + seeds.append(("left", block_pos - 2, color)) + if right_seed: + color = rng.randint(1, 9) + question[block_pos + block_size + 1] = color + seeds.append(("right", block_pos + block_size + 1, color)) + + # Create answer with duplicated blocks + answer = question.copy() + + for side, seed_pos, color in seeds: + if side == "left": + # For left seed, blocks end at seed + end_pos = seed_pos + while end_pos >= 0: + start_pos = end_pos - block_size + 1 + for pos in range(max(0, start_pos), end_pos + 1): + answer[pos] = color + if start_pos < 1: + break + end_pos = start_pos - 2 # -1 for gap + else: # side == "right" + # For right seed, blocks start at seed + start_pos = seed_pos + while start_pos < size: + for offset in range(min(block_size, size - start_pos)): + answer[start_pos + offset] = color + if start_pos + block_size + 1 >= size: + break + start_pos = start_pos + block_size + 1 # +1 for gap + + return {"input": question, "output": answer} + + +def task_fill_from_pixel(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a pixel fills in one direction until hitting another pixel.""" + block_size = rng.randint(3, 6) + if block_size >= size - 2: + return None + + # Position block with space for seed + block_pos = rng.randint(1, size - block_size - 1) + + # Create input + question = gen_field(size) + + # Place main block + block_color = rng.randint(1, 9) + for i in range(block_size): + question[block_pos + i] = block_color + + # Place seed pixel and determine fill direction + seed_color = rng.randint(1, 9) + while seed_color == block_color: + seed_color = rng.randint(1, 9) + + is_left = rng.random() < 0.5 + + if is_left: + question[block_pos - 1] = seed_color + else: + question[block_pos + block_size] = seed_color + + # Create answer with fill + answer = question.copy() + + if is_left: + # Fill from seed to left border + for i in range(block_pos): + answer[i] = seed_color + else: + # Fill from seed to right border + for i in range(block_pos + block_size, size): + answer[i] = seed_color + + return {"input": question, "output": answer} + + +def task_mark_size_two_blocks(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where size-2 blocks are marked with surrounding pixels.""" + blocks = [] + pos = 0 + + # Generate blocks with minimum gap of 2 + while pos < size: + if rng.random() < 0.4: + block_size = rng.randint(1, 3) + # Check if we have space for block and potential markers + needed_space = block_size + (2 if block_size == 2 else 0) + if pos + needed_space < size: + blocks.append((pos, block_size)) + pos += block_size + 2 # Minimum gap of 2 + + pos += 1 + + if len(blocks) < 2: + return None + + # Verify gaps between blocks (including markers) + valid = True + for i in range(len(blocks) - 1): + pos1, size1 = blocks[i] + pos2, _ = blocks[i + 1] + needed_gap = 3 if size1 == 2 else 2 + if pos2 - (pos1 + size1) < needed_gap: + valid = False + break + if not valid: + return None + + # Create input with blocks + question = gen_field(size) + for pos, block_size in blocks: + # Place block + for i in range(block_size): + question[pos + i] = 1 + + # Create answer with markers + answer = question.copy() + for pos, block_size in blocks: + if block_size == 2: + # Add markers for size 2 blocks + if pos > 0: + answer[pos - 1] = 3 + if pos + block_size < size: + answer[pos + block_size] = 3 + + return {"input": question, "output": answer} + + +def task_fill_until_collision(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where pixels fill empty space until collision.""" + # At least 4 positions for meaningful puzzle + if size < 4: + return None + + is_left = rng.random() < 0.5 + question = gen_field(size) + + # Place the side marker + if is_left: + question[0] = 5 + else: + question[size - 1] = 5 + + # Place 2-4 random pixels + num_pixels = rng.randint(2, 4) + positions = [] + + if is_left: + # Skip first position + for _ in range(num_pixels): + while True: + pos = rng.randint(1, size - 1) + if pos not in positions: + positions.append(pos) + break + else: + # Skip last position + for _ in range(num_pixels): + while True: + pos = rng.randint(0, size - 2) + if pos not in positions: + positions.append(pos) + break + + # Color random pixels + for pos in positions: + question[pos] = rng.randint(1, 9) + + positions.sort() + + # Create answer + answer = question.copy() + + if is_left: + # Fill right from each pixel + prev_pos = 0 # Start from marker + for pos in positions: + color = question[pos] + # Fill from previous position to current + for i in range(prev_pos + 1, pos): + answer[i] = color + prev_pos = pos + else: + # Fill left from each pixel + prev_pos = size - 1 # Start from marker + for pos in reversed(positions): + color = question[pos] + # Fill from current position to previous + for i in range(pos + 1, prev_pos): + answer[i] = color + prev_pos = pos + + return {"input": question, "output": answer} + + +def task_repeat_pattern_full(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where a pattern is repeated to fill the space.""" + # Generate initial pattern + pattern_size = rng.randint(2, 5) + pattern = [rng.randint(1, 9) for _ in range(pattern_size)] + + # Calculate total size needed for 2 repetitions + double_size = pattern_size * 2 + if double_size >= size: + return None + + # Create input with 2 repetitions + question = gen_field(size) + for i in range(pattern_size): + question[i] = pattern[i] + question[i + pattern_size] = pattern[i] + + # Create answer with maximum repetitions + answer = gen_field(size) + pos = 0 + while pos + pattern_size <= size: + for i in range(pattern_size): + answer[pos + i] = pattern[i] + pos += pattern_size + + # Fill remaining space (if any) with pattern elements + for i in range(pos, size): + answer[i] = pattern[i - pos] + + return {"input": question, "output": answer} + + +def task_gravity_weighted_colors(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where color 2 is heavier than color 1 in gravity.""" + # Generate random field with only colors 1 and 2 + question = [rng.randint(1, 2) if rng.random() < 0.5 else 0 for _ in range(size)] + + # Count colors + count_1 = sum(1 for x in question if x == 1) + count_2 = sum(1 for x in question if x == 2) + + # Create answer with sorted colors + answer = gen_field(size) + + # Place heavier color 2 first + for i in range(count_2): + answer[i] = 2 + + # Then place color 1 + for i in range(count_1): + answer[count_2 + i] = 1 + + return {"input": question, "output": answer} + + +def task_color_left_half_blocks(rng: Random, size: int) -> Optional[Dict[str, List[int]]]: + """Generate a task where left half of blocks are colored differently.""" + pos = 0 + question = gen_field(size) + blocks = [] + + # Generate blocks with gap 1 + while pos < size: + if rng.random() < 0.4: + block_size = rng.randint(2, 8) + if pos + block_size >= size: + break + + blocks.append((pos, block_size)) + for i in range(block_size): + question[pos + i] = 2 + pos += block_size + 1 # block size + gap + else: + pos += 1 + + if len(blocks) < 2: + return None + + # Create answer with half-colored blocks + answer = question.copy() + for pos, block_size in blocks: + half_size = block_size // 2 + for i in range(half_size): + answer[pos + i] = 8 + + return {"input": question, "output": answer} + + +def task_mirror(task_result: Optional[Dict[str, List[int]]]) -> Optional[Dict[str, List[int]]]: + """Mirror the input and output arrays of a task result.""" + if task_result is None: + return None + return {"input": list(reversed(task_result["input"])), "output": list(reversed(task_result["output"]))} + + +def task_inverse(task_result: Optional[Dict[str, List[int]]]) -> Optional[Dict[str, List[int]]]: + """Swap the input and output arrays of a task result.""" + if task_result is None: + return None + return {"input": task_result["output"], "output": task_result["input"]} + + +def task_identity(task_result: Optional[Dict[str, List[int]]]) -> Optional[Dict[str, List[int]]]: + """Return the task result unchanged.""" + return task_result + + +# Table of all ARC 1D task functions with their parameters +ARC_1D_TASKS = { + # Move tasks - right direction + "move_1pix_solid_right": (task_move_n_pix, {"move_pix": 1, "solid": True}), + "move_2pix_solid_right": (task_move_n_pix, {"move_pix": 2, "solid": True}), + "move_3pix_solid_right": (task_move_n_pix, {"move_pix": 3, "solid": True}), + "move_4pix_solid_right": (task_move_n_pix, {"move_pix": 4, "solid": True}), + "move_1pix_colorful_right": (task_move_n_pix, {"move_pix": 1, "solid": False}), + "move_2pix_colorful_right": (task_move_n_pix, {"move_pix": 2, "solid": False}), + "move_3pix_colorful_right": (task_move_n_pix, {"move_pix": 3, "solid": False}), + "move_4pix_colorful_right": (task_move_n_pix, {"move_pix": 4, "solid": False}), + # Move tasks - left direction (mirrored) + "move_1pix_solid_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 1, "solid": True}, + ), + "move_2pix_solid_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 2, "solid": True}, + ), + "move_3pix_solid_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 3, "solid": True}, + ), + "move_4pix_solid_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 4, "solid": True}, + ), + "move_1pix_colorful_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 1, "solid": False}, + ), + "move_2pix_colorful_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 2, "solid": False}, + ), + "move_3pix_colorful_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 3, "solid": False}, + ), + "move_4pix_colorful_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix(rng, size, **kwargs)), + {"move_pix": 4, "solid": False}, + ), + # Move wrapped tasks - right direction + "move_1pix_solid_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 1, "solid": True}), + "move_2pix_solid_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 2, "solid": True}), + "move_3pix_solid_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 3, "solid": True}), + "move_4pix_solid_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 4, "solid": True}), + "move_1pix_colorful_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 1, "solid": False}), + "move_2pix_colorful_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 2, "solid": False}), + "move_3pix_colorful_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 3, "solid": False}), + "move_4pix_colorful_wrapped_right": (task_move_n_pix_wrapped, {"move_pix": 4, "solid": False}), + # Move wrapped tasks - left direction (mirrored) + "move_1pix_solid_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 1, "solid": True}, + ), + "move_2pix_solid_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 2, "solid": True}, + ), + "move_3pix_solid_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 3, "solid": True}, + ), + "move_4pix_solid_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 4, "solid": True}, + ), + "move_1pix_colorful_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 1, "solid": False}, + ), + "move_2pix_colorful_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 2, "solid": False}, + ), + "move_3pix_colorful_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 3, "solid": False}, + ), + "move_4pix_colorful_wrapped_left": ( + lambda rng, size, **kwargs: task_mirror(task_move_n_pix_wrapped(rng, size, **kwargs)), + {"move_pix": 4, "solid": False}, + ), + # Gravity tasks - right direction + "gravity_right": (task_gravity, {}), + "gravity_counting_right": (task_gravity_counting, {}), + "gravity_antigravity_right": (task_gravity_antigravity, {}), + "gravity_one_step_right": (task_gravity_one_step, {}), + "gravity_weighted_colors_right": (task_gravity_weighted_colors, {}), + # Gravity tasks - left direction (mirrored) + "gravity_left": (lambda rng, size, **kwargs: task_mirror(task_gravity(rng, size, **kwargs)), {}), + "gravity_counting_left": (lambda rng, size, **kwargs: task_mirror(task_gravity_counting(rng, size, **kwargs)), {}), + "gravity_antigravity_left": ( + lambda rng, size, **kwargs: task_mirror(task_gravity_antigravity(rng, size, **kwargs)), + {}, + ), + "gravity_one_step_left": (lambda rng, size, **kwargs: task_mirror(task_gravity_one_step(rng, size, **kwargs)), {}), + "gravity_weighted_colors_left": ( + lambda rng, size, **kwargs: task_mirror(task_gravity_weighted_colors(rng, size, **kwargs)), + {}, + ), + # Block tasks + "block_touch_dot": (task_block_touch_dot, {}), + "block_touch_dot_1pix": (task_block_touch_dot_n_pix, {"move_pix": 1}), + "block_touch_dot_2pix": (task_block_touch_dot_n_pix, {"move_pix": 2}), + "block_touch_dot_3pix": (task_block_touch_dot_n_pix, {"move_pix": 3}), + "block_touch_dot_4pix": (task_block_touch_dot_n_pix, {"move_pix": 4}), + "block_scale_to_dot": (task_block_scale_to_dot, {}), + "block_and_noise_remove": (task_block_and_noise_remove, {}), + "block_and_noise_remove_inside": (task_block_and_noise_remove_inside, {}), + "move_block_by_own_size": (task_move_block_by_own_size, {}), + # Pattern tasks + "two_points_and_fill": (task_two_points_and_fill, {}), + "two_points_and_fill_inv": ( + lambda rng, size, **kwargs: task_inverse(task_two_points_and_fill(rng, size, **kwargs)), + {}, + ), + "copy_block_to_dots": (task_copy_block_to_dots, {}), + "copy_block_to_dots_colors": (task_copy_block_to_dots_colors, {}), + "repeat_pattern_full": (task_repeat_pattern_full, {}), + # Reflection tasks + "reflect_block_with_border_pixel": (task_reflect_block_with_border_pixel, {}), + "reflect_block_random": (task_reflect_block_with_border_pixel_random, {}), + "reflect_block_around_dot": (task_reflect_block_around_dot, {}), + # Color tasks + "paint_biggest_block": (task_paint_biggest_block, {}), + "recolor_blocks_by_size": (task_recolor_blocks_by_size, {}), + "change_to_five": (task_change_to_five, {}), + "recolor_blocks_from_palette": (task_recolor_blocks_from_palette, {}), + "color_left_half_blocks": (task_color_left_half_blocks, {}), + # Sorting tasks + "sort_blocks_by_size": (task_sort_blocks_by_size, {}), + "sort_complete_sequence": (task_sort_complete_sequence, {}), + # Fill tasks + "duplicate_block_from_seeds": (task_duplicate_block_from_seeds, {}), + "fill_from_pixel": (task_fill_from_pixel, {}), + "fill_until_collision": (task_fill_until_collision, {}), + # Marking tasks + "mark_size_two_blocks": (task_mark_size_two_blocks, {}), +} diff --git a/reasoning_gym/data/words.csv b/reasoning_gym/data/words.csv index ed237b47..11f35901 100644 --- a/reasoning_gym/data/words.csv +++ b/reasoning_gym/data/words.csv @@ -1,5308 +1,5308 @@ -3_letter,4_letter,5_letter -ABA,ABET,ABACK -ABS,ABLE,ABAFT -ACE,ABLY,ABASE -ACT,ABUT,ABASH -ADD,ACAI,ABATE -ADO,ACED,ABAYA -ADS,ACER,ABBEY -ADZ,ACES,ABBOT -AFT,ACHE,ABEAM -AGA,ACHY,ABETS -AGE,ACID,ABHOR -AGO,ACME,ABIDE -AHA,ACNE,ABLED -AHI,ACRE,ABLER -AID,ACTS,ABODE -AIL,ADDS,ABORT -AIM,ADDY,ABOUT -AIR,ADZE,ABOVE -AKA,AEON,ABUSE -ALA,AERO,ABUTS -ALE,AFAR,ABUZZ -ALL,AFRO,ABYSS -ALP,AGAR,ACERS -ALS,AGAS,ACHED -ALT,AGED,ACHES -ALU,AGES,ACIDS -AMP,AGIN,ACING -ANA,AGIO,ACINI -AND,AGOG,ACKEE -ANT,AGUE,ACORN -ANY,AHEM,ACRES -APE,AHIS,ACRID -APP,AHOY,ACTED -APT,AIDE,ACTIN -ARC,AIDS,ACTOR -ARE,AILS,ACUTE -ARK,AIMS,ADAGE -ARM,AIRS,ADAPT -ART,AIRY,ADDED -ASH,AJAR,ADDER -ASK,AKIN,ADDLE -ASP,ALAS,ADEPT -ASS,ALEC,ADIEU -ATE,ALES,ADIOS -AVE,ALGA,ADMAN -AWE,ALLY,ADMEN -AWL,ALMA,ADMIN -AXE,ALMS,ADMIT -AYE,ALOE,ADOBE -BAA,ALOO,ADOBO -BAD,ALSO,ADOPT -BAE,ALTO,ADORE -BAG,ALUM,ADORN -BAH,AMEN,ADULT -BAM,AMID,ADZES -BAN,AMMO,AEGIS -BAO,AMPS,AEONS -BAP,AMYL,AERIE -BAR,ANAL,AFFIX -BAS,ANCE,AFIRE -BAT,ANEW,AFOOT -BAY,ANKH,AFORE -BED,ANON,AFOUL -BEE,ANTE,AFROS -BEG,ANTI,AFTER -BEN,ANTS,AGAIN -BET,ANUS,AGAPE -BIB,APED,AGATE -BID,APES,AGAVE -BIG,APEX,AGENT -BIN,APPS,AGGRO -BIO,APSE,AGILE -BIT,AQUA,AGING -BIZ,ARCH,AGISM -BOA,ARCO,AGIST -BOB,ARCS,AGITA -BOD,AREA,AGLOW -BOG,ARES,AGONY -BOO,ARIA,AGORA -BOP,ARID,AGREE -BOT,ARKS,AHEAD -BOW,ARMS,AHOLD -BOX,ARMY,AIDED -BOY,ARSE,AIDES -BRA,ARTS,AILED -BRO,ARTY,AIMED -BUB,ASHY,AIOLI -BUD,ASKS,AIRED -BUG,ASPS,AIRER -BUM,ATOM,AISLE -BUN,ATOP,AITCH -BUS,AULD,ALACK -BUT,AUNT,ALARM -BUY,AURA,ALBUM -BYE,AUTO,ALDER -CAB,AVER,ALECS -CAD,AVID,ALERT -CAL,AVOW,ALGAE -CAM,AWAY,ALIAS -CAN,AWED,ALIBI -CAP,AWES,ALIEN -CAR,AWLS,ALIGN -CAT,AWOL,ALIKE -CAW,AWRY,ALIVE -CAY,AXED,ALKIE -CHA,AXEL,ALKYL -CHI,AXES,ALLAY -CIG,AXIL,ALLEY -CIS,AXIS,ALLOT -COB,AXLE,ALLOW -COD,AXON,ALLOY -COG,AYES,ALOES -COL,BAAS,ALOFT -CON,BABA,ALOHA -COO,BABE,ALONE -COP,BABY,ALONG -COR,BACK,ALOOF -COS,BADE,ALOUD -COT,BAES,ALPHA -COW,BAGS,ALTAR -COX,BAHT,ALTER -COY,BAIL,ALTOS -CRY,BAIT,ALUMS -CUB,BAKE,ALWAY -CUD,BALD,AMASS -CUP,BALE,AMAZE -CUR,BALK,AMBER -CUT,BALL,AMBIT -CUZ,BALM,AMBLE -DAB,BAND,AMEBA -DAD,BANE,AMEND -DAG,BANG,AMENS -DAH,BANK,AMIDE -DAL,BANS,AMIGO -DAM,BAPS,AMINE -DAP,BARB,AMINO -DAW,BARD,AMISS -DAY,BARE,AMITY -DEF,BARF,AMNIO -DEL,BARK,AMONG -DEN,BARM,AMOUR -DEW,BARN,AMPED -DEX,BARS,AMPLE -DID,BASE,AMPLY -DIE,BASH,AMUCK -DIG,BASK,AMUSE -DIM,BASS,ANCHO -DIN,BATH,ANGEL -DIP,BATS,ANGER -DIS,BAUD,ANGLE -DOC,BAWL,ANGLO -DOE,BAYS,ANGRY -DOG,BEAD,ANGST -DOH,BEAK,ANIME -DOL,BEAM,ANION -DOM,BEAN,ANISE -DON,BEAR,ANKLE -DOO,BEAT,ANNAL -DOS,BEAU,ANNEX -DOT,BECK,ANNOY -DOX,BEDS,ANNUL -DRY,BEEF,ANODE -DUB,BEEN,ANTED -DUD,BEEP,ANTES -DUE,BEER,ANTIC -DUG,BEES,ANTIS -DUH,BEET,ANTSY -DUN,BEGS,ANVIL -DUO,BELL,AORTA -DUX,BELT,APACE -DYE,BEND,APART -DZO,BENS,APHID -EAR,BENT,APING -EAT,BERG,APNEA -EAU,BERM,APPAL -EBB,BEST,APPLE -ECO,BETA,APPLY -EEK,BETS,APRES -EEL,BEVY,APRON -EFF,BIAS,APSES -EFT,BIBB,APTLY -EGG,BIBS,ARBOR -EGO,BIDE,ARCED -EKE,BIDS,ARCUS -ELF,BIER,ARDOR -ELK,BIFF,AREAS -ELM,BIGS,ARECA -EMO,BIKE,ARENA -EMU,BILE,ARETE -END,BILK,ARGAN -EON,BILL,ARGON -ERA,BIND,ARGOT -ERG,BING,ARGUE -ERM,BINS,ARIAS -ERR,BIOS,ARISE -ESS,BIRD,ARMED -EST,BIRL,ARMOR -ETA,BIRO,AROID -EVE,BITE,AROMA -EWE,BITS,AROSE -EYE,BLAB,ARRAY -FAB,BLAG,ARROW -FAD,BLAH,ARSED -FAG,BLED,ARSES -FAM,BLEW,ARSON -FAN,BLIP,ARTIC -FAR,BLOB,ARTSY -FAT,BLOC,ASANA -FAV,BLOG,ASCOT -FAX,BLOT,ASHEN -FED,BLOW,ASHES -FEE,BLUB,ASIDE -FEM,BLUE,ASKED -FEN,BLUR,ASKEW -FEW,BOAR,ASPEN -FEY,BOAS,ASPIC -FEZ,BOAT,ASSAY -FIB,BOBS,ASSES -FIE,BODE,ASSET -FIG,BODY,ASTER -FIN,BOGS,ASTIR -FIR,BOGY,ATLAS -FIT,BOHO,ATOLL -FIX,BOIL,ATOMS -FLU,BOLD,ATONE -FLY,BOLE,ATRIA -FOB,BOLL,ATTIC -FOE,BOLO,AUDIO -FOG,BOLT,AUDIT -FOP,BOMB,AUGER -FOR,BONA,AUGHT -FOX,BOND,AUGUR -FRO,BONE,AUNTS -FRY,BONG,AUNTY -FUG,BONK,AURAL -FUN,BONY,AURAS -FUR,BOOB,AUTOS -GAB,BOOK,AUXIN -GAD,BOOM,AVAIL -GAG,BOON,AVANT -GAL,BOOR,AVERS -GAP,BOOS,AVERT -GAR,BOOT,AVIAN -GAS,BOPS,AVOID -GAT,BORE,AVOWS -GAY,BORN,AWAIT -GED,BOSH,AWAKE -GEE,BOSS,AWARD -GEL,BOTH,AWARE -GEM,BOTS,AWASH -GEN,BOUT,AWAYS -GEO,BOWL,AWFUL -GET,BOWS,AWING -GIB,BOXY,AWOKE -GIF,BOYS,AXIAL -GIG,BOZO,AXILS -GIN,BRAD,AXING -GIT,BRAG,AXIOM -GNU,BRAN,AXION -GOB,BRAS,AXLES -GOD,BRAT,AXONS -GOO,BRAY,AZURE -GOT,BRED,BAAED -GRR,BREW,BABAS -GUM,BRIE,BABEL -GUN,BRIG,BABES -GUT,BRIM,BACCY -GUV,BRIO,BACKS -GUY,BRIS,BACON -GYM,BRIT,BADDY -GYP,BROS,BADGE -HAD,BROW,BADLY -HAG,BRUH,BAGEL -HAH,BRUT,BAGGY -HAM,BUBO,BAILS -HAS,BUCK,BAIRN -HAT,BUDS,BAITS -HAW,BUFF,BAIZE -HAY,BUGS,BAKED -HEM,BULB,BAKER -HEN,BULK,BAKES -HER,BULL,BALDY -HEW,BUMF,BALED -HEX,BUMP,BALER -HEY,BUMS,BALES -HID,BUNG,BALKS -HIE,BUNK,BALKY -HIM,BUNS,BALLS -HIP,BUNT,BALLY -HIS,BUOY,BALMS -HIT,BURL,BALMY -HMM,BURN,BALSA -HOB,BURP,BALTI -HOC,BURR,BANAL -HOD,BURS,BANDS -HOE,BURY,BANDY -HOG,BUSH,BANGS -HON,BUSK,BANJO -HOP,BUST,BANKS -HOS,BUSY,BANNS -HOT,BUTE,BANTS -HOW,BUTS,BARBS -HUB,BUTT,BARDS -HUE,BUYS,BARED -HUG,BUZZ,BARER -HUH,BYES,BARES -HUM,BYRE,BARFS -HUN,BYTE,BARGE -HUP,CABS,BARKS -HUT,CAFE,BARMY -ICE,CAFF,BARNS -ICK,CAGE,BARON -ICY,CAKE,BARRE -IDS,CALF,BASAL -IFS,CALL,BASED -ILK,CALM,BASER -ILL,CALS,BASES -IMP,CAME,BASIC -INK,CAMI,BASIL -INN,CAMO,BASIN -INS,CAMP,BASIS -ION,CAMS,BASKS -IRE,CANE,BASSO -IRK,CANS,BASTE -ISH,CANT,BATCH -ISM,CAPE,BATED -ISO,CAPO,BATHE -ITS,CAPS,BATHS -IVY,CARB,BATIK -JAB,CARD,BATON -JAG,CARE,BATTY -JAM,CARK,BAULK -JAR,CARP,BAWDY -JAW,CARS,BAWLS -JAY,CART,BAYED -JET,CASE,BAYOU -JIB,CASH,BEACH -JIG,CASK,BEADS -JOB,CAST,BEADY -JOE,CATS,BEAKS -JOG,CAUL,BEAMS -JOT,CAVA,BEAMY -JOY,CAVE,BEANO -JUG,CAWS,BEANS -JUS,CAYS,BEARD -JUT,CEDE,BEARS -KEG,CELL,BEAST -KEN,CELT,BEATS -KEY,CENT,BEAUS -KID,CERT,BEAUT -KIN,CHAD,BEAUX -KIP,CHAI,BEBOP -KIR,CHAP,BECKS -KIT,CHAR,BEECH -KOI,CHAT,BEEFS -LAB,CHAV,BEEFY -LAD,CHEF,BEEPS -LAG,CHEM,BEERS -LAH,CHEW,BEERY -LAM,CHIA,BEETS -LAP,CHIC,BEFIT -LAT,CHIN,BEGAN -LAV,CHIP,BEGAT -LAW,CHIS,BEGET -LAX,CHIT,BEGIN -LAY,CHOC,BEGOT -LEA,CHOP,BEGUN -LED,CHOW,BEIGE -LEE,CHUB,BEING -LEG,CHUG,BELAY -LEI,CHUM,BELCH -LET,CIAO,BELIE -LEY,CIGS,BELLE -LID,CIST,BELLS -LIE,CITE,BELLY -LIP,CITY,BELOW -LIS,CLAD,BELTS -LIT,CLAG,BENCH -LOB,CLAM,BENDS -LOG,CLAN,BENDY -LOO,CLAP,BENNE -LOP,CLAW,BENNY -LOT,CLAY,BENTO -LOW,CLEF,BERET -LOX,CLIP,BERGS -LUG,CLIT,BERKS -LUX,CLOD,BERMS -LYE,CLOG,BERRY -MAC,CLOP,BERTH -MAD,CLOT,BESET -MAG,CLOY,BESOM -MAL,CLUB,BESTS -MAM,CLUE,BETAS -MAN,COAL,BETEL -MAP,COAT,BEVEL -MAR,COAX,BEVVY -MAT,COBB,BEZEL -MAW,COBS,BHAJI -MAX,COCA,BIALY -MAY,COCK,BIBLE -MED,COCO,BICEP -MEG,CODA,BIDDY -MEH,CODE,BIDED -MEN,COED,BIDET -MET,COGS,BIERS -MEW,COHO,BIFFS -MIC,COIL,BIGHT -MID,COIN,BIGLY -MIL,COIR,BIGOT -MIX,COKE,BIJOU -MOB,COLA,BIKED -MOD,COLD,BIKER -MOM,COLT,BIKES -MON,COMA,BIKIE -MOO,COMB,BILBY -MOP,COME,BILGE -MOS,COMP,BILKS -MOW,CONE,BILLS -MUD,CONK,BILLY -MUG,CONS,BIMBO -MUM,CONY,BINDI -MUS,COOK,BINDS -NAB,COOL,BINGE -NAG,COON,BINGO -NAH,COOP,BINGS -NAN,COOS,BIOME -NAP,COOT,BIOTA -NAT,COPE,BIPED -NAV,COPS,BIRCH -NAY,COPY,BIRDS -NEE,CORD,BIRLS -NET,CORE,BIROS -NEW,CORK,BIRTH -NIB,CORM,BISON -NIL,CORN,BITCH -NIP,CORS,BITER -NIT,COSH,BITES -NIX,COST,BITTY -NOB,COSY,BIVVY -NOD,COTE,BLABS -NOG,COTS,BLACK -NOH,COUP,BLADE -NON,COVE,BLAGS -NOR,COWL,BLAHS -NOS,COWS,BLAME -NOT,COXA,BLAND -NOW,COZY,BLANK -NTH,CRAB,BLARE -NUN,CRAG,BLASE -NUT,CRAM,BLAST -OAF,CRAP,BLAZE -OAK,CRAY,BLEAK -OAR,CRED,BLEAT -OAT,CREW,BLEED -OBO,CRIB,BLEEP -ODD,CRIT,BLEND -ODE,CROC,BLESS -ODS,CROP,BLEST -OFF,CROW,BLIMP -OFT,CRUD,BLIND -OHM,CRUX,BLING -OHS,CUBE,BLINK -OIK,CUBS,BLIPS -OIL,CUES,BLISS -OLD,CUFF,BLITZ -OLE,CUKE,BLOAT -ONE,CULL,BLOBS -ONO,CULT,BLOCK -OOF,CUPS,BLOCS -OOH,CURB,BLOGS -OPS,CURD,BLOKE -OPT,CURE,BLOND -ORA,CURL,BLOOD -ORB,CURS,BLOOM -ORC,CURT,BLOOP -ORE,CUSP,BLOTS -ORT,CUSS,BLOWN -OUD,CUTE,BLOWS -OUR,CUTS,BLOWY -OUT,CYAN,BLUBS -OVA,CYST,BLUER -OWE,CZAR,BLUES -OWL,DABS,BLUEY -OWN,DADA,BLUFF -OWT,DADO,BLUNT -PAD,DADS,BLURB -PAL,DAFT,BLURS -PAN,DAGS,BLURT -PAP,DAIS,BLUSH -PAR,DALE,BOARD -PAS,DAME,BOARS -PAT,DAMN,BOAST -PAW,DAMP,BOATS -PAX,DAMS,BOBBY -PAY,DANG,BOCCE -PEA,DANK,BODED -PEC,DANS,BODES -PEE,DAPS,BODGE -PEG,DARE,BOGAN -PEN,DARK,BOGEY -PEP,DARN,BOGGY -PER,DART,BOGIE -PES,DASH,BOGUS -PET,DATA,BOHEA -PEW,DATE,BOHOS -PHI,DAUB,BOILS -PHO,DAWN,BOING -PIC,DAYS,BOINK -PIE,DAZE,BOKEH -PIG,DEAD,BOLES -PIN,DEAF,BOLLS -PIP,DEAL,BOLTS -PIS,DEAN,BOLUS -PIT,DEAR,BOMBE -PIX,DEBT,BOMBS -PLY,DECK,BONCE -POD,DECO,BONDS -POI,DEED,BONED -POM,DEEM,BONER -POO,DEEP,BONES -POP,DEER,BONGO -POT,DEFO,BONGS -POW,DEFT,BONKS -POX,DEFY,BONNY -PRE,DEKE,BONUS -PRO,DELI,BOOBS -PRY,DELL,BOOBY -PSI,DELS,BOOED -PUB,DEMO,BOOKS -PUD,DENS,BOOMS -PUG,DENT,BOOMY -PUN,DENY,BOONS -PUP,DESI,BOORS -PUS,DESK,BOOST -PUT,DEUS,BOOTH -PWN,DEWY,BOOTS -QAT,DHAL,BOOTY -QUA,DHOW,BOOZE -RAD,DIAL,BOOZY -RAG,DIBS,BOPPY -RAH,DICE,BORAX -RAM,,BORED -RAN,DIED,BORER -RAP,DIES,BORES -RAT,DIET,BORNE -RAW,DIFF,BORON -RAY,DIGS,BOSKY -REC,DIKE,BOSOM -RED,DILL,BOSON -REF,DIME,BOSSY -REG,DIMS,BOSUN -REM,DINE,BOTCH -REP,DING,BOTHY -RES,DINK,BOTOX -REV,DINO,BOUGH -REX,DINS,BOULE -REZ,DINT,BOUND -RHO,DIPS,BOURN -RIB,DIRE,BOUTS -RID,DIRK,BOVID -RIG,DIRT,BOWED -RIM,DISC,BOWEL -RIP,DISH,BOWER -ROB,DISK,BOWLS -ROD,DISS,BOXED -ROE,DITZ,BOXER -ROO,DIVA,BOXES -ROT,DIVE,BOXTY -ROW,DIVS,BOZOS -RUB,DOBS,BRAAI -RUE,DOCK,BRACE -RUG,DOCS,BRACT -RUM,DODO,BRAGS -RUN,DOER,BRAID -RUT,DOES,BRAIN -RYE,DOFF,BRAKE -SAC,DOGE,BRAND -SAD,DOGS,BRASH -SAG,DOJO,BRASS -SAP,DOLE,BRATS -SAT,DOLL,BRAVE -SAW,DOLT,BRAVO -SAX,DOME,BRAWL -SAY,DONE,BRAWN -SEA,DONG,BRAYS -SEC,DONS,BREAD -SEE,DOOM,BREAK -SET,DOOR,BREAM -SEW,DOPE,BREED -SEX,DORK,BRENT -SHE,DORM,BREVE -SHH,DORY,BREWS -SHY,DOSA,BRIAR -SIB,DOSE,BRIBE -SIC,DOSH,BRICK -SIM,DOSS,BRIDE -SIN,DOST,BRIEF -SIP,DOTE,BRIER -SIR,DOTH,BRIGS -SIS,DOTS,BRILL -SIT,DOUR,BRIMS -SIX,DOVE,BRINE -SKA,DOWN,BRING -SKI,DOXX,BRINK -SKY,DOZE,BRINY -SLY,DOZY,BRISK -SOB,DRAB,BRITS -SOD,DRAG,BROAD -SOL,DRAM,BROCH -SON,DRAT,BROCK -SOP,DRAW,BROIL -SOS,DRAY,BROKE -SOT,DREW,BRONC -SOU,DREY,BROOD -SOW,DRIP,BROOK -SOX,DROP,BROOM -SOY,DRUB,BROTH -SOZ,DRUG,BROWN -SPA,DRUM,BROWS -SPY,DUAL,BRUIT -STY,DUBS,BRUME -SUB,DUCK,BRUNT -SUE,DUCT,BRUSH -SUM,DUDE,BRUTE -SUN,DUDS,BUCKS -SUP,DUEL,BUDDY -SUS,DUES,BUDGE -TAB,DUET,BUFFS -TAD,DUFF,BUGGY -TAG,DUKE,BUGLE -TAJ,DULL,BUILD -TAM,DULY,BUILT -TAN,DUMB,BULBS -TAP,DUMP,BULGE -TAR,DUNE,BULKS -TAS,DUNG,BULKY -TAT,DUNK,BULLA -TAU,DUNS,BULLS -TAX,DUOS,BULLY -TEA,DUPE,BUMPH -TED,DURA,BUMPS -TEE,DUSK,BUMPY -TEL,DUST,BUNCH -TEN,DUTY,BUNDT -THE,DYAD,BUNGS -THO,DYED,BUNKS -THY,DYER,BUNNY -TIC,DYES,BUNTS -TIE,DYKE,BUOYS -TIN,DZOS,BURET -TIP,EACH,BURKA -TIS,EARL,BURLS -TIT,EARN,BURLY -TOD,EARS,BURNS -TOE,EASE,BURNT -TOG,EAST,BURPS -TOM,EASY,BURQA -TON,EATS,BURRS -TOO,EAVE,BURSA -TOP,EBBS,BURST -TOR,ECHO,BUSBY -TOT,EDDY,BUSED -TOW,EDGE,BUSES -TOY,EDGY,BUSHY -TRY,EDIT,BUSKS -TUB,EELS,BUSTS -TUG,EFFS,BUSTY -TUM,EFTS,BUTCH -TUN,EGAD,BUTTE -TUT,EGGS,BUTTS -TUX,EGGY,BUTTY -TWO,EGOS,BUXOM -UFO,EKED,BUYER -UGH,EKES,BUZZY -UMM,ELAN,BYLAW -UMP,ELKS,BYRES -UNI,ELMS,BYTES -UPS,ELSE,BYWAY -URN,EMIC,CABAL -USE,EMIR,CABBY -UTE,EMIT,CABER -VAC,EMOS,CABIN -VAN,EMUS,CABLE -VAR,ENDS,CACAO -VAT,ENVY,CACHE -VAX,EONS,CACTI -VEE,EPEE,CADDY -VEG,EPIC,CADET -VET,EPOS,CADGE -VEX,ERAS,CADRE -VIA,ERGO,CAFES -VIE,ERGS,CAFFS -VIM,ERRS,CAGED -VIS,ETAS,CAGES -VOG,ETCH,CAGEY -VOW,ETIC,CAIRN -VOX,EURO,CAJUN -WAD,EVEN,CAKED -WAG,EVER,CAKES -WAN,EVES,CALLA -WAR,EVIL,CALLS -WAS,EWER,CALMS -WAT,EWES,CALVE -WAX,EXAM,CALYX -WAY,EXES,CAMAS -WEB,EXIT,CAMEL -WED,EXPO,CAMEO -WEE,EYED,CAMPS -WET,EYES,CAMPY -WHO,FABS,CANAL -WHY,FACE,CANDY -WIG,FACT,CANED -WIN,FADE,CANES -WIT,FADO,CANID -WIZ,FADS,CANNA -WOE,FAFF,CANNY -WOK,FAGS,CANOE -WON,FAIL,CANON -WOO,FAIN,CANTO -WOP,FAIR,CAPED -WOT,FAKE,CAPER -WOW,FALL,CAPES -WRY,FAME,CAPEX -YAH,FAMS,CAPON -YAK,FANG,CAPOS -YAM,FANS,CAPUT -YAP,FARE,CARAT -YAW,FARL,CARBS -YAY,FARM,CARDI -YEA,FART,CARDS -YEH,FAST,CARDY -YEN,FATE,CARED -YEP,FATS,CARER -YER,FAUN,CARES -YES,FAUX,CARET -YET,FAVA,CARGO -YEW,FAVE,CAROB -YIN,FAVS,CAROL -YIP,FAWN,CAROM -YOB,FAZE,CARPI -YON,FEAR,CARPS -YOU,FEAT,CARRY -YUK,FECK,CARTE -YUM,FEDS,CARTS -YUP,FEED,CARVE -ZAG,FEEL,CASED -ZAP,FEES,CASES -ZED,FEET,CASKS -ZEE,FELL,CASTE -ZEN,FELT,CASTS -ZIG,FEND,CATCH -ZIP,FENS,CATER -ZIT,FERN,CATTY -ZOO,FESS,CAULK -ZZZ,FEST,CAULS -,FETA,CAUSE -,FETE,CAVED -,FEUD,CAVES -,FIAT,CAVIL -,FIBS,CAWED -,FIEF,CEASE -,FIFE,CECUM -,FIGS,CEDAR -,FILE,CEDED -,FILL,CEDES -,FILM,CELEB -,FILO,CELLO -,FIND,CELLS -,FINE,CENTS -,FINK,CERTS -,FINS,CHADS -,FIRE,CHAFE -,FIRM,CHAFF -,FIRN,CHAIN -,FIRS,CHAIR -,FISC,CHALK -,FISH,CHAMP -,FIST,CHANT -,FITS,CHAOS -,FIVE,CHAPS -,FIZZ,CHARD -,FLAB,CHARM -,FLAG,CHARS -,FLAK,CHART -,FLAN,CHARY -,FLAP,CHASE -,FLAT,CHASM -,FLAW,CHATS -,FLAX,CHAVS -,FLAY,CHEAP -,FLEA,CHEAT -,FLED,CHECK -,FLEE,CHEEK -,FLEW,CHEEP -,FLEX,CHEER -,FLIP,CHEFS -,FLIT,CHEMO -,FLOE,CHESS -,FLOG,CHEST -,FLOP,CHEWS -,FLOW,CHEWY -,FLUB,CHICK -,FLUE,CHIDE -,FLUX,CHIEF -,FOAL,CHILD -,FOAM,CHILE -,FOBS,CHILI -,FOCI,CHILL -,FOES,CHIME -,FOGS,CHIMP -,FOGY,CHINA -,FOHN,CHINE -,FOIL,CHINK -,FOLD,CHINO -,FOLK,CHINS -,FOND,CHIPS -,FONT,CHIRP -,FOOD,CHITS -,FOOL,CHIVE -,FOOT,CHOCK -,FOPS,CHOCS -,FORA,CHOIR -,FORD,CHOKE -,FORE,CHOMP -,FORK,CHOPS -,FORM,CHORD -,FORT,CHORE -,FOUL,CHOSE -,FOUR,CHOWS -,FOWL,CHUCK -,FOXY,CHUFF -,FRAT,CHUGS -,FRAY,CHUMP -,FREE,CHUMS -,FRET,CHUNK -,FRIT,CHURN -,FROG,CHURR -,FROM,CHUTE -,FUEL,CHYME -,FUGU,CIDER -,FULL,CIGAR -,FUME,CIGGY -,FUND,CILIA -,FUNK,CILLS -,FURL,CINCH -,FURS,CIRCA -,FURY,CISCO -,FUSE,CISSY -,FUSS,CISTS -,FUTZ,CITED -,FUZE,CITES -,FUZZ,CIVET -,GABS,CIVIC -,GADS,CIVIL -,GAFF,CIVVY -,GAGA,CLACK -,GAGE,CLADE -,GAGS,CLADS -,GAIN,CLAIM -,GAIT,CLAMP -,GALA,CLAMS -,GALE,CLANG -,GALL,CLANK -,GALS,CLANS -,GAME,CLAPS -,GAMY,CLARY -,GANG,CLASH -,GAOL,CLASP -,GAPE,CLASS -,GAPS,CLAWS -,GARB,CLEAN -,GASH,CLEAR -,GASP,CLEAT -,GATE,CLEFS -,GATS,CLEFT -,GAVE,CLERK -,GAWD,CLICK -,GAWK,CLIFF -,GAWP,CLIMB -,GAYS,CLIME -,GAZE,CLINE -,GEAR,CLING -,GEDS,CLINK -,GEEK,CLIPS -,GEES,CLITS -,GEEZ,CLOAK -,GELD,CLOCK -,GELS,CLODS -,GEMS,CLOGS -,GENE,CLOMP -,GENS,CLONE -,GENT,CLOPS -,GERM,CLOSE -,GETS,CLOTH -,GHAT,CLOTS -,GHEE,CLOUD -,GIBE,CLOUT -,GIFT,CLOVE -,GIGA,CLOWN -,GIGS,CLOYS -,GILD,CLUBS -,GILL,CLUCK -,GILT,CLUED -,GIMP,CLUES -,GINS,CLUMP -,GIRD,CLUNG -,GIRL,CLUNK -,GIRO,COACH -,GIRT,COALS -,GIST,COAST -,GITE,COATI -,GITS,COATS -,GIVE,COBRA -,GLAD,COCKS -,GLAM,COCKY -,GLEE,COCOA -,GLEN,CODAS -,GLIA,CODEC -,GLIB,CODED -,GLOB,CODER -,GLOM,CODES -,GLOP,CODEX -,GLOW,CODON -,GLUE,COEDS -,GLUG,COIGN -,GLUM,COILS -,GLUT,COINS -,GNAT,COKES -,GNAW,COLAS -,GNUS,COLDS -,GOAD,COLEY -,GOAL,COLIC -,GOAT,COLON -,GOBO,COLOR -,GOBS,COLTS -,GODS,COMAS -,GOER,COMBE -,GOES,COMBI -,GOLD,COMBO -,GOLF,COMBS -,GONE,COMER -,GONG,COMES -,GOOD,COMET -,GOOF,COMFY -,GOON,COMIC -,GOOP,COMMA -,GORE,COMMS -,GORP,COMPS -,GORY,CONCH -,GOSH,CONDO -,GOTH,CONED -,GOUT,CONES -,GOWN,CONEY -,GRAB,CONGA -,GRAD,CONIC -,GRAM,CONKS -,GRAN,CONVO -,GRAY,COOED -,GREW,COOKS -,GREY,COOLS -,GRID,COONS -,GRIM,COOPS -,GRIN,COOTS -,GRIP,COPED -,GRIT,COPES -,GROG,COPRA -,GROK,COPSE -,GROW,CORAL -,GRUB,CORDS -,GUAC,CORED -,GUFF,CORES -,GULF,CORGI -,GULL,CORKS -,GULP,CORKY -,GUMS,CORMS -,GUNK,CORNS -,GUNS,CORNY -,GURU,CORPS -,GUSH,COSTA -,GUST,COSTS -,GUTS,COTTA -,GUYS,COUCH -,GYMS,COUGH -,GYRE,COULD -,GYRI,COUNT -,GYRO,COUPE -,HAAR,COUPS -,HACK,COURT -,HADJ,COUTH -,HAFT,COVED -,HAGS,COVEN -,HAHA,COVER -,HAIL,COVES -,HAIR,COVET -,HAJI,COVEY -,HAJJ,COVID -,HAKA,COWED -,HAKE,COWER -,HALE,COWLS -,HALF,COWRY -,HALL,COXED -,HALO,COXES -,HALT,COYER -,HAMS,COYLY -,HAND,COYPU -,HANG,COZEN -,HANK,CRABS -,HARD,CRACK -,HARE,CRAFT -,HARK,CRAGS -,HARM,CRAIC -,HARP,CRAMP -,HART,CRAMS -,HASH,CRANE -,HASP,CRANK -,HAST,CRAPE -,HATE,CRAPS -,HATH,CRASH -,HATS,CRASS -,HAUL,CRATE -,HAVE,CRAVE -,HAWK,CRAWL -,HAYS,CRAYS -,HAZE,CRAZE -,HAZY,CRAZY -,HEAD,CREAK -,HEAL,CREAM -,HEAP,CREDO -,HEAR,CREED -,HEAT,CREEK -,HECK,CREEP -,HEED,CREME -,HEEL,CREPE -,HEFT,CREPT -,HEIR,CRESS -,HELD,CREST -,HELL,CREWS -,HELM,CRIBS -,HELP,CRICK -,HEMP,CRIED -,HEMS,CRIER -,HENS,CRIES -,HERB,CRIME -,HERD,CRIMP -,HERE,CRISP -,HERO,CROAK -,HERS,CROCK -,HEWN,CROCS -,HEWS,CROFT -,HICK,CRONE -,HIDE,CRONY -,HIED,CROOK -,HIES,CROON -,HIGH,CROPS -,HIKE,CRORE -,HILL,CROSS -,HILT,CROUP -,HIND,CROWD -,HINT,CROWN -,HIPS,CROWS -,HIRE,CRUDE -,HISS,CRUEL -,HITS,CRUET -,HIVE,CRUMB -,HIYA,CRUMP -,HMMM,CRUNK -,HOAX,CRUSH -,HOBO,CRUST -,HOBS,CRYPT -,HOCK,CUBBY -,HODS,CUBED -,HOED,CUBES -,HOES,CUBIC -,HOGS,CUBIT -,HOKI,CUFFS -,HOLD,CUKES -,HOLE,CULLS -,HOLM,CULPA -,HOLS,CULTS -,HOLT,CUMIN -,HOLY,CUPPA -,HOME,CURBS -,HOMO,CURDS -,HONE,CURED -,HONK,CURES -,HOOD,CURIE -,HOOF,CURIO -,HOOK,CURLS -,HOOP,CURLY -,HOOT,CURRY -,HOPE,CURSE -,HOPS,CURVE -,HORN,CURVY -,HOSE,CUSHY -,HOST,CUTER -,HOTS,CUTEY -,HOUR,CUTIE -,HOVE,CUTIS -,HOWL,CUTUP -,HOYA,CWTCH -,HUBS,CYBER -,HUED,CYCAD -,HUES,CYCLE -,HUFF,CYCLO -,HUGE,CYDER -,HUGS,CYNIC -,HULA,CYSTS -,HULK,CZARS -,HULL,DADDY -,HUMP,DADOS -,HUMS,DAFFY -,HUNG,DAGGY -,HUNK,DAILY -,HUNT,DAIRY -,HURL,DAISY -,HURT,DALES -,HUSH,DALLY -,HUSK,DAMES -,HUTS,DAMNS -,HYMN,DAMPS -,HYPE,DANCE -,HYPO,DANDY -,IAMB,DARED -,IBEX,DARES -,IBIS,DARNS -,ICED,DARTS -,ICES,DATED -,ICKY,DATES -,ICON,DATUM -,IDEA,DAUBE -,IDEM,DAUBS -,IDES,DAUNT -,IDLE,DAWNS -,IDLY,DAZED -,IDOL,DEALS -,IDYL,DEALT -,IFFY,DEANS -,IKAT,DEARS -,IKON,DEATH -,ILEX,DEBAR -,ILIA,DEBIT -,ILLS,DEBTS -,IMAM,DEBUG -,IMPS,DEBUT -,INCH,DECAF -,INFO,DECAL -,INKS,DECAY -,INKY,DECKS -,INNS,DECOR -,INTO,DECOY -,IONS,DECRY -,IOTA,DEEDS -,IRIS,DEEMS -,IRKS,DEETS -,IRON,DEFAT -,ISLE,DEFER -,ISMS,DEFFO -,ITCH,DEIFY -,ITEM,DEIGN -,JABS,DEISM -,JACK,DEITY -,JADE,DEKED -,JAGS,DEKES -,JAIL,DELAY -,JAMB,DELIS -,JAMS,DELLS -,JANE,DELTA -,JAPE,DELTS -,JARS,DELVE -,JAVA,DEMON -,JAWS,DEMOS -,JAYS,DEMUR -,JAZZ,DENIM -,JEAN,DENSE -,JEEP,DENTS -,JEER,DEPOT -,JEEZ,DEPTH -,JELL,DERBY -,JERK,DERMA -,JEST,DESIS -,JETS,DESKS -,JIBE,DETER -,JIBS,DETOX -,JIFF,DEUCE -,JIGS,DEVIL -,JILT,DEVON -,JINN,DHOTI -,JINX,DIALS -,JIVE,DIARY -,JIZZ,DICED -,JOBS,DICES -,JOCK,DICEY -,JOES, -,JOEY,DICKY -,JOGS,DICOT -,JOHN,DICTA -,JOIN,DIDDY -,JOKE,DIETS -,JOLT,DIGIT -,JOSH,DIKES -,JOSS,DILDO -,JOTS,DILLY -,JOWL,DIMES -,JOYS,DIMLY -,JUDO,DINAR -,JUGS,DINED -,JUJU,DINER -,JUKE,DINES -,JUMP,DINGO -,JUNK,DINGS -,JURY,DINGY -,JUST,DINKS -,JUTE,DINKY -,JUTS,DINTS -,KALE,DIODE -,KART,DIPPY -,KATA,DIPSO -,KEEL,DIRGE -,KEEN,DIRKS -,KEEP,DIRTY -,KEGS,DISCO -,KELP,DISCS -,KENS,DISHY -,KEPT,DISKS -,KERB,DITCH -,KEYS,DITSY -,KHAN,DITTO -,KHAT,DITTY -,KICK,DITZY -,KIDS,DIVAN -,KILL,DIVAS -,KILN,DIVED -,KILO,DIVER -,KILT,DIVES -,KIND,DIVIS -,KING,DIVOT -,KINK,DIVVY -,KIPS,DIZZY -,KIRK,DJINN -,KIRS,DOBRO -,KISS,DOCKS -,KITE,DODGE -,KITH,DODGY -,KITS,DODOS -,KIVA,DOERS -,KIWI,DOEST -,KNEE,DOETH -,KNEW,DOFFS -,KNIT,DOGGO -,KNOB,DOGGY -,KNOT,DOGIE -,KNOW,DOGMA -,KOOK,DOILY -,KRIS,DOING -,LABS,DOJOS -,LACE,DOLED -,LACK,DOLES -,LACY,DOLLS -,LADS,DOLLY -,LADY,DOLMA -,LAGS,DOLTS -,LAID,DOMED -,LAIN,DOMES -,LAIR,DONEE -,LAKE,DONGA -,LAKH,DONGS -,LAMA,DONNA -,LAMB,DONOR -,LAME,DONUT -,LAMP,DOOMS -,LAND,DOOMY -,LANE,DOONA -,LANK,DOORS -,LAPS,DOOZY -,LARD,DOPES -,LARK,DOPEY -,LARP,DORKS -,LASH,DORKY -,LASS,DORMS -,LAST,DORSA -,LATE,DOSAS -,LATH,DOSES -,LATS,DOTED -,LAUD,DOTES -,LAVA,DOTTY -,LAVS,DOUBT -,LAWN,DOUCE -,LAWS,DOUGH -,LAYS,DOULA -,LAZE,DOUSE -,LAZY,DOVES -,LEAD,DOWDY -,LEAF,DOWEL -,LEAK,DOWER -,LEAN,DOWNS -,LEAP,DOWNY -,LEAS,DOWRY -,LECH,DOWSE -,LEDE,DOXED -,LEEK,DOXES -,LEER,DOYEN -,LEET,DOZED -,LEFT,DOZEN -,LEGS,DOZER -,LEIS,DOZES -,LEND,DRAFT -,LENS,DRAGS -,LENT,DRAIN -,LESS,DRAKE -,LEST,DRAMA -,LETS,DRAMS -,LEVY,DRANK -,LEWD,DRAPE -,LIAR,DRAWL -,LIBS,DRAWN -,LICE,DRAWS -,LICK,DRAYS -,LIDO,DREAD -,LIDS,DREAM -,LIED,DRECK -,LIEF,DREGS -,LIEN,DRESS -,LIES,DREYS -,LIEU,DRIBS -,LIFE,DRIED -,LIFT,DRIER -,LIKE,DRIES -,LILT,DRIFT -,LILY,DRILL -,LIMA,DRILY -,LIMB,DRINK -,LIME,DRIPS -,LIMN,DRIVE -,LIMO,DROID -,LIMP,DROIT -,LINE,DROLL -,LING,DROME -,LINK,DRONE -,LINO,DROOL -,LINT,DROOP -,LION,DROPS -,LIPO,DROSS -,LIPS,DROVE -,LIRA,DROWN -,LISP,DRUGS -,LIST,DRUID -,LITE,DRUMS -,LIVE,DRUNK -,LOAD,DRUPE -,LOAF,DRYAD -,LOAN,DRYER -,LOBE,DRYLY -,LOBS,DUALS -,LOCH,DUCAL -,LOCI,DUCHY -,LOCK,DUCKS -,LOCO,DUCKY -,LODE,DUCTS -,LOFT,DUDES -,LOGE,DUELS -,LOGO,DUETS -,LOGS,DUFFS -,LOGY,DUKES -,LOIN,DULLS -,LOLL,DULLY -,LOLZ,DULSE -,LOME,DUMBS -,LONE,DUMMY -,LONG,DUMPS -,LOOK,DUMPY -,LOOM,DUNCE -,LOON,DUNES -,LOOP,DUNNO -,LOOS,DUNNY -,LOOT,DUPED -,LOPE,DUPES -,LOPS,DUPLE -,LORD,DURRY -,LORE,DURUM -,LORN,DUSKY -,LOSE,DUSTS -,LOSS,DUSTY -,LOST,DUTCH -,LOTH,DUVET -,LOTS,DUXES -,LOUD,DWARF -,LOUT,DWEEB -,LOVE,DWELL -,LOWS,DWELT -,LUBE,DYADS -,LUCK,DYERS -,LUGE,DYING -,LUGS,DYKES -,LULL,EAGER -,LULU,EAGLE -,LUMP,EARED -,LUNG,EARLS -,LURE,EARLY -,LURK,EARNS -,LUSH,EARTH -,LUST,EASED -,LUTE,EASEL -,LUXE,EASES -,LYES,EATEN -,LYME,EATER -,LYNX,EAVES -,LYRE,EBBED -,MACE,EBONY -,MACK,EBOOK -,MACS,ECLAT -,MADE,EDEMA -,MAGE,EDGED -,MAGI,EDGES -,MAGS,EDICT -,MAID,EDIFY -,MAIL,EDITS -,MAIM,EDUCE -,MAIN,EEJIT -,MAKE,EERIE -,MALA,EFFED -,MALE,EGADS -,MALL,EGGED -,MALT,EGRET -,MAMA,EIGHT -,MAMS,EJECT -,MANE,EKING -,MANO,ELATE -,MANS,ELBOW -,MANY,ELDER -,MAPS,ELECT -,MARE,ELEGY -,MARK,ELFIN -,MARL,ELIDE -,MART,ELITE -,MASA,ELOPE -,MASH,ELUDE -,MASK,ELVEN -,MASS,ELVER -,MAST,ELVES -,MATE,EMAIL -,MATH,EMBED -,MATS,EMBER -,MATT,EMCEE -,MAUL,EMEND -,MAWS,EMERY -,MAXI,EMIRS -,MAYO,EMITS -,MAYS,EMMER -,MAZE,EMMET -,MAZY,EMOJI -,MEAD,EMOTE -,MEAL,EMPTY -,MEAN,ENACT -,MEAT,ENDED -,MEDS,ENDOW -,MEEK,ENEMA -,MEET,ENEMY -,MEGA,ENJOY -,MEGS,ENNUI -,MELD,ENOKI -,MELT,ENROL -,MEME,ENSUE -,MEMO,ENTER -,MEND,ENTRY -,MENU,ENVOY -,MEOW,EPEES -,MERC,EPICS -,MERE,EPOCH -,MESA,EPOXY -,MESH,EQUAL -,MESS,EQUID -,META,EQUIP -,METE,ERASE -,METH,ERECT -,MEWL,ERHUS -,MEWS,ERODE -,MEZE,ERRED -,MICA,ERROR -,MICE,ERUPT -,MICS,ESKER -,MIDI,ESSAY -,MIEN,ESTER -,MIKE,ETHER -,MILD,ETHIC -,MILE,ETHOS -,MILF,ETHYL -,MILK,ETUDE -,MILL,ETYMA -,MILO,EUROS -,MIME,EVADE -,MIND,EVENS -,MINE,EVENT -,MINI,EVERT -,MINK,EVERY -,MINT,EVICT -,MINX,EVILS -,MIRE,EVOKE -,MIRY,EWERS -,MISO,EXACT -,MISS,EXALT -,MIST,EXAMS -,MITE,EXCEL -,MITT,EXECS -,MOAN,EXERT -,MOAT,EXILE -,MOBS,EXIST -,MOCK,EXITS -,MODE,EXPAT -,MODS,EXPEL -,MOJO,EXPOS -,MOLD,EXTOL -,MOLE,EXTRA -,MOLL,EXUDE -,MOLT,EXULT -,MOLY,EXURB -,MOMS,EYING -,MONK,EYRIE -,MONO,EZINE -,MOOD,FABLE -,MOOK,FACED -,MOON,FACES -,MOOR,FACET -,MOOS,FACIA -,MOOT,FACTS -,MOPE,FADDY -,MOPS,FADED -,MORE,FADES -,MORN,FAFFS -,MOSH,FAGOT -,MOSS,FAILS -,MOST,FAINT -,MOTE,FAIRS -,MOTH,FAIRY -,MOVE,FAITH -,MOWN,FAKED -,MOWS,FAKER -,MUCH,FAKES -,MUCK,FAKIR -,MUFF,FALLS -,MUGS,FALSE -,MULE,FAMED -,MULL,FAMES -,MUMS,FANCY -,MUNG,FANGS -,MUNI,FANNY -,MUON,FAQIR -,MURK,FARAD -,MUSE,FARCE -,MUSH,FARED -,MUSK,FARES -,MUSS,FARLS -,MUST,FARMS -,MUTE,FARRO -,MUTT,FARTS -,MWAH,FASTS -,MYTH,FATAL -,NAAN,FATED -,NABS,FATES -,NADS,FATSO -,NAFF,FATTY -,NAGS,FATWA -,NAIL,FAULT -,NAME,FAUNA -,NANA,FAUNS -,NANO,FAVES -,NANS,FAVOR -,NAPE,FAWNS -,NAPS,FAXED -,NARC,FAXES -,NARK,FAYRE -,NARY,FAZED -,NATS,FAZES -,NAVE,FEARS -,NAVY,FEAST -,NAYS,FEATS -,NEAP,FECAL -,NEAR,FECES -,NEAT,FEEDS -,NECK,FEELS -,NEED,FEIGN -,NEEP,FEINT -,NEGS,FELID -,NEON,FELLA -,NERD,FELLS -,NESS,FELON -,NEST,FELTS -,NETS,FEMME -,NEWS,FEMUR -,NEWT,FENCE -,NEXT,FENDS -,NIBS,FERAL -,NICE,FERNS -,NICK,FERRY -,NIGH,FESTS -,NINE,FETAL -,NIPS,FETCH -,NITE,FETED -,NITS,FETES -,NOBS,FETID -,NOCK,FETUS -,NODE,FEUDS -,NODS,FEVER -,NOEL,FEWER -,NOES,FIATS -,NOIR,FIBER -,NOMS,FIBRE -,NONE,FIBRO -,NONI,FICHE -,NOOB,FICUS -,NOOK,FIEFS -,NOON,FIELD -,NOPE,FIEND -,NORI,FIERY -,NORM,FIFTH -,NOSE,FIFTY -,NOSH,FIGGY -,NOSY,FIGHT -,NOTE,FILCH -,NOUN,FILED -,NOUS,FILES -,NOVA,FILET -,NOWT,FILLS -,NUDE,FILLY -,NUKE,FILMS -,NUMB,FILMY -,NUNS,FILTH -,NUTS,FINAL -,OAFS,FINCH -,OAKS,FINDS -,OARS,FINED -,OATH,FINER -,OATS,FINES -,OBEY,FINKS -,OBIT,FINNY -,OBOE,FIORD -,OBVS,FIRED -,ODDS,FIRES -,ODES,FIRMS -,ODOR,FIRST -,OFFS,FIRTH -,OGLE,FISHY -,OGRE,FISKS -,OHMS,FISTS -,OIKS,FITCH -,OILS,FITLY -,OILY,FIVER -,OINK,FIVES -,OKAY,FIXED -,OLDE,FIXER -,OMEN,FIXES -,OMIT,FIZZY -,ONCE,FJORD -,ONES,FLACK -,ONLY,FLAGS -,ONTO,FLAIL -,ONUS,FLAIR -,ONYX,FLAKE -,OOPS,FLAKY -,OOZE,FLAME -,OOZY,FLAMS -,OPAL,FLANK -,OPEN,FLANS -,OPUS,FLAPS -,ORAL,FLARE -,ORBS,FLASH -,ORCS,FLASK -,ORES,FLATS -,ORGY,FLAWS -,OURS,FLAYS -,OUST,FLEAS -,OUTS,FLECK -,OVAL,FLEEK -,OVEN,FLEES -,OVER,FLEET -,OWED,FLESH -,OWES,FLICK -,OWLS,FLIER -,OWNS,FLIES -,OXEN,FLING -,PACE,FLINT -,PACK,FLIPS -,PACT,FLIRT -,PADS,FLITS -,PAGE,FLOAT -,PAID,FLOCK -,PAIL,FLOES -,PAIN,FLOGS -,PAIR,FLOOD -,PALE,FLOOR -,PALL,FLOPS -,PALM,FLORA -,PALS,FLOSS -,PANE,FLOUR -,PANG,FLOUT -,PANS,FLOWN -,PANT,FLOWS -,PAPA,FLUBS -,PAPS,FLUES -,PARA,FLUFF -,PARE,FLUID -,PARK,FLUKE -,PARS,FLUKY -,PART,FLUME -,PASS,FLUNG -,PAST,FLUNK -,PATE,FLUSH -,PATH,FLUTE -,PATS,FLYBY -,PAVE,FLYER -,PAWN,FOALS -,PAWS,FOAMS -,PAYS,FOAMY -,PEAK,FOCAL -,PEAL,FOCUS -,PEAR,FOEHN -,PEAS,FOGEY -,PEAT,FOGGY -,PECK,FOILS -,PECS,FOIST -,PEED,FOLDS -,PEEL,FOLIO -,PEEP,FOLKS -,PEER,FOLLY -,PEGS,FONTS -,PELT,FOODS -,PENS,FOODY -,PENT,FOOLS -,PERK,FOOTS -,PERT,FOOTY -,PESO,FORAY -,PEST,FORCE -,PETS,FORDS -,PEWS,FORES -,PHIS,FOREX -,PICK,FORGE -,PIER,FORGO -,PIES,FORKS -,PIGS,FORMS -,PIKE,FORTE -,PILE,FORTH -,PILL,FORTS -,PIMP,FORTY -,PINE,FORUM -,PINK,FOSSA -,PINS,FOULS -,PINT,FOUND -,PIPE,FOUNT -,PISS,FOURS -,PITH,FOVEA -,PITS,FOWLS -,PITY,FOXED -,PLAN,FOXES -,PLAT,FOYER -,PLAY,FRACK -,PLEA,FRAIL -,PLOD,FRAME -,PLOT,FRANC -,PLOW,FRANK -,PLOY,FRASS -,PLUG,FRATS -,PLUM,FRAUD -,PLUS,FRAYS -,PODS,FREAK -,POEM,FREED -,POET,FREER -,POIS,FREES -,POKE,FRESH -,POLE,FRETS -,POLL,FRIAR -,POLO,FRIED -,POMP,FRIER -,POND,FRIES -,PONS,FRILL -,PONY,FRISK -,POOH,FRITZ -,POOL,FRIZZ -,POOR,FROCK -,POPE,FROGS -,POPS,FROND -,PORE,FRONT -,PORK,FROSH -,PORT,FROST -,POSE,FROTH -,POST,FROWN -,POTS,FROZE -,POUR,FRUIT -,POUT,FRUMP -,PRAM,FRYER -,PRAY,FUBSY -,PREP,FUDGE -,PREY,FUELS -,PRIG,FUGAL -,PRIM,FUGGY -,PROD,FUGLY -,PROM,FUGUE -,PROP,FULLY -,PROS,FUMED -,PROW,FUMES -,PSIS,FUMET -,PSST,FUNDI -,PUBE,FUNDS -,PUBS,FUNGI -,PUCE,FUNGO -,PUCK,FUNKY -,PUDS,FUNNY -,PUFF,FURLS -,PUGS,FUROR -,PUKE,FURRY -,PULI,FURZE -,PULL,FUSED -,PULP,FUSES -,PUMA,FUSSY -,PUMP,FUSTY -,PUNK,FUTON -,PUNS,FUZZY -,PUNT,GABBY -,PUNY,GABLE -,PUPA,GAFFE -,PUPS,GAFFS -,PURE,GAGED -,PURI,GAGES -,PURL,GAILY -,PURR,GAINS -,PUSH,GAITS -,PUSS,GALAH -,PUTS,GALAS -,PUTT,GALES -,PUTZ,GALLS -,PYRE,GAMED -,QUAD,GAMER -,QUAY,GAMES -,QUID,GAMMA -,QUIM,GAMMY -,QUIP,GAMUT -,QUIT,GANGS -,QUIZ,GANJA -,RACE,GAOLS -,RACK,GAPED -,RACY,GAPES -,RAFT,GARBS -,RAGA,GARDA -,RAGE,GASES -,RAGS,GASPS -,RAGU,GASSY -,RAID,GATED -,RAIL,GATES -,RAIN,GATOR -,RAKE,GAUDY -,RALE,GAUGE -,RAMI,GAUNT -,RAMP,GAUZE -,RAMS,GAUZY -,RAND,GAVEL -,RANG,GAWKS -,RANI,GAWKY -,RANK,GAWPS -,RANT,GAYER -,RAPE,GAYLY -,RAPS,GAZED -,RAPT,GAZER -,RARE,GAZES -,RASH,GEARS -,RASP,GECKO -,RATA,GEEKS -,RATE,GEEKY -,RATS,GEESE -,RAVE,GELDS -,RAWS,GENES -,RAYS,GENIC -,RAZE,GENIE -,READ,GENII -,REAL,GENOA -,REAM,GENRE -,REAP,GENTS -,REAR,GENUS -,RECS,GEOID -,REDO,GERMS -,REDS,GERMY -,REED,GESSO -,REEF,GETUP -,REEK,GHATS -,REEL,GHOST -,REFS,GHOUL -,REGS,GIANT -,REIN,GIBES -,RELY,GIDDY -,REND,GIFTS -,RENO,GILDS -,RENT,GILET -,REPO,GILLS -,REPS,GILTS -,REST,GIMME -,RETE,GIMPS -,REVS,GIPSY -,RIAD,GIRDS -,RIAL,GIRLS -,RIBS,GIRLY -,RICE,GIROS -,RICH,GIRTH -,RICK,GISMO -,RIDE,GITES -,RIDS,GIVEN -,RIFE,GIVER -,RIFF,GIVES -,RIFT,GIZMO -,RIGS,GLADE -,RILE,GLADS -,RILL,GLAND -,RIME,GLANS -,RIMS,GLARE -,RIMY,GLASS -,RIND,GLAZE -,RING,GLEAM -,RINK,GLEAN -,RIOT,GLENS -,RIPE,GLIDE -,RIPS,GLINT -,RISE,GLITZ -,RISK,GLOAT -,RITE,GLOBE -,RIVE,GLOBS -,ROAD,GLOOM -,ROAM,GLOOP -,ROAN,GLORY -,ROAR,GLOSS -,ROBE,GLOVE -,ROBS,GLOWS -,ROCK,GLUED -,RODE,GLUES -,RODS,GLUEY -,ROIL,GLUON -,ROLE,GLUTE -,ROLL,GLUTS -,ROMA,GLYPH -,ROMP,GNARL -,ROOF,GNASH -,ROOK,GNATS -,ROOM,GNAWS -,ROOS,GNOME -,ROOT,GOADS -,ROPE,GOALS -,ROPY,GOATS -,RORT,GOBBY -,ROSE,GODLY -,ROSY,GOERS -,ROTA,GOETH -,ROTE,GOFER -,ROTI,GOING -,ROTS,GOLDS -,ROUT,GOLEM -,ROUX,GOLFS -,ROVE,GOLLY -,ROWS,GONAD -,RUBE,GONER -,RUBS,GONGS -,RUBY,GONZO -,RUCK,GOODS -,RUDE,GOODY -,RUED,GOOEY -,RUES,GOOFS -,RUFF,GOOFY -,RUGS,GOONS -,RUIN,GOOSE -,RULE,GORED -,RUMP,GORES -,RUMS,GORGE -,RUNE,GORSE -,RUNG,GOUGE -,RUNS,GOURD -,RUNT,GOUTY -,RUSE,GOWNS -,RUSH,GRABS -,RUSK,GRACE -,RUST,GRADE -,RUTS,GRADS -,RYOT,GRAFT -,SAAG,GRAIL -,SACK,GRAIN -,SACS,GRAMS -,SAFE,GRAND -,SAGA,GRANS -,SAGE,GRANT -,SAGO,GRAPE -,SAGS,GRAPH -,SAID,GRASP -,SAIL,GRASS -,SAKE,GRATE -,SAKI,GRAVE -,SALE,GRAVY -,SALT,GRAYS -,SAME,GRAZE -,SAND,GREAT -,SANE,GREBE -,SANG,GREED -,SANK,GREEK -,SANS,GREEN -,SAPS,GREET -,SARI,GREYS -,SARK,GRIDS -,SASH,GRIEF -,SASS,GRIFT -,SATE,GRILL -,SATI,GRIME -,SAVE,GRIMY -,SAWN,GRIND -,SAWS,GRINS -,SAYS,GRIOT -,SCAB,GRIPE -,SCAM,GRIPS -,SCAN,GRIST -,SCAR,GRITS -,SCAT,GROAN -,SCOT,GROAT -,SCRY,GROIN -,SCUD,GROOM -,SCUM,GROPE -,SCUT,GROSS -,SEAL,GROUP -,SEAM,GROUT -,SEAR,GROVE -,SEAS,GROWL -,SEAT,GROWN -,SECS,GROWS -,SECT,GRUBS -,SEED,GRUEL -,SEEK,GRUFF -,SEEM,GRUMP -,SEEN,GRUNT -,SEEP,GUANO -,SEER,GUARD -,SEES,GUAVA -,SELF,GUESS -,SELL,GUEST -,SEMI,GUIDE -,SEND,GUILD -,SENS,GUILE -,SENT,GUILT -,SEPT,GUISE -,SERA,GULAG -,SERF,GULCH -,SESH,GULES -,SETS,GULFS -,SETT,GULLS -,SEWN,GULLY -,SEWS,GULPS -,SEXY,GUMBO -,SHAD,GUMMY -,SHAG,GUNGE -,SHAH,GUNKY -,SHAM,GUNNY -,SHAW,GUPPY -,SHEA,GURUS -,SHED,GUSHY -,SHES,GUSSY -,SHEW,GUSTO -,SHIM,GUSTS -,SHIN,GUSTY -,SHIP,GUTSY -,SHIV,GUTTY -,SHOD,GUYOT -,SHOE,GYPSY -,SHOO,GYRED -,SHOP,GYROS -,SHOT,GYRUS -,SHOW,HABIT -,SHUN,HACKS -,SHUT,HADST -,SICK,HAIKU -,SICS,HAILS -,SIDE,HAIRS -,SIFT,HAIRY -,SIGH,HAJIS -,SIGN,HAJJI -,SIGS,HAKAS -,SILK,HAKES -,SILL,HALAL -,SILO,HALLO -,SILT,HALLS -,SIMP,HALOS -,SIMS,HALTS -,SINE,HALVA -,SING,HALVE -,SINK,HAMMY -,SINS,HANDS -,SIPS,HANDY -,SIRE,HANGS -,SIRS,HANKY -,SITE,HAPPY -,SITS,HARAM -,SIZE,HARDS -,SKEP,HARDY -,SKEW,HARED -,SKID,HAREM -,SKIM,HARES -,SKIN,HARKS -,SKIP,HARMS -,SKIS,HARPS -,SKIT,HARPY -,SKUA,HARRY -,SKYR,HARSH -,SLAB,HARTS -,SLAG,HASPS -,SLAM,HASTE -,SLAP,HASTY -,SLAT,HATCH -,SLAW,HATED -,SLAY,HATER -,SLED,HATES -,SLEW,HAULM -,SLID,HAULS -,SLIM,HAUNT -,SLIP,HAUTE -,SLIT,HAVEN -,SLOB,HAVES -,SLOE,HAVOC -,SLOG,HAWKS -,SLOP,HAZED -,SLOT,HAZEL -,SLOW,HAZES -,SLUG,HEADS -,SLUM,HEADY -,SLUR,HEALS -,SLUT,HEAPS -,SMOG,HEARD -,SMUG,HEARS -,SMUT,HEART -,SNAG,HEATH -,SNAP,HEATS -,SNIP,HEAVE -,SNIT,HEAVY -,SNOB,HEDGE -,SNOG,HEEDS -,SNOT,HEELS -,SNOW,HEFTS -,SNUB,HEFTY -,SNUG,HEIGH -,SOAK,HEIRS -,SOAP,HEIST -,SOAR,HELIO -,SOBA,HELIX -,SOBS,HELLO -,SOCA,HELMS -,SOCK,HELPS -,SODA,HENCE -,SODS,HENCH -,SOFA,HENGE -,SOFT,HENNA -,SOIL,HENRY -,SOLD,HERBS -,SOLE,HERBY -,SOLO,HERDS -,SOLS,HERON -,SOMA,HERTZ -,SOME,HEWED -,SONG,HEXED -,SONS,HEXES -,SOOK,HICKS -,SOON,HIDES -,SOOT,HIGHS -,SOPS,HIJAB -,SORE,HIJRA -,SORT,HIKED -,SOTS,HIKER -,SOUK,HIKES -,SOUL,HILLS -,SOUP,HILLY -,SOUR,HILTS -,SOUS,HILUM -,SOWN,HINDS -,SOWS,HINGE -,SOYA,HINKY -,SPAM,HINTS -,SPAN,HIPPO -,SPAR,HIPPY -,SPAS,HIRED -,SPAT,HIRES -,SPAY,HITCH -,SPEC,HIVED -,SPED,HIVES -,SPEW,HOARD -,SPIN,HOARY -,SPIT,HOBBY -,SPIV,HOBOS -,SPOD,HOCKS -,SPOT,HOCUS -,SPRY,HOICK -,SPUD,HOIST -,SPUN,HOKEY -,SPUR,HOKUM -,STAB,HOLDS -,STAG,HOLED -,STAR,HOLES -,STAT,HOLLY -,STAY,HOLTS -,STEM,HOMED -,STEP,HOMER -,STEW,HOMES -,STIR,HOMEY -,STOP,HOMIE -,STOW,HOMOS -,STUB,HONED -,STUD,HONES -,STUN,HONEY -,STYE,HONKS -,SUBS,HONOR -,SUCH,HOOCH -,SUCK,HOODS -,SUDS,HOODY -,SUED,HOOEY -,SUES,HOOFS -,SUET,HOOKS -,SUIT,HOOKY -,SULK,HOOPS -,SUMO,HOOTS -,SUMP,HOPED -,SUMS,HOPES -,SUNG,HORDE -,SUNK,HORNS -,SUNS,HORNY -,SUPS,HORSE -,SUQS,HORSY -,SURE,HOSED -,SURF,HOSEL -,SUSS,HOSER -,SWAB,HOSES -,SWAG,HOSTA -,SWAM,HOSTS -,SWAN,HOTEL -,SWAP,HOTLY -,SWAT,HOUND -,SWAY,HOURS -,SWIG,HOUSE -,SWIM,HOVEL -,SWIZ,HOVER -,SWOP,HOWDY -,SWOT,HOWLS -,SWUM,HOYAS -,SYNC,HUBBY -,SYNE,HUFFS -,TABI,HUFFY -,TABS,HULAS -,TACK,HULKS -,TACO,HULLO -,TACT,HULLS -,TAGS,HUMAN -,TAIL,HUMIC -,TAKA,HUMID -,TAKE,HUMOR -,TALC,HUMPH -,TALE,HUMPS -,TALK,HUMPY -,TALL,HUMUS -,TAME,HUNCH -,TAMP,HUNKS -,TANG,HUNKY -,TANK,HUNTS -,TANS,HURLS -,TAPE,HURRY -,TAPS,HURTS -,TAPU,HUSKS -,TARE,HUSKY -,TARN,HUSSY -,TARO,HUTCH -,TARP,HYDRA -,TARS,HYDRO -,TART,HYENA -,TASE,HYGGE -,TASH,HYMEN -,TASK,HYMNS -,TATS,HYOID -,TAUS,HYPED -,TAUT,HYPER -,TAXI,HYPES -,TEAK,HYPHA -,TEAL,HYSON -,TEAM,IAMBS -,TEAR,ICHOR -,TEAS,ICIER -,TEAT,ICILY -,TECH,ICING -,TEED,ICONS -,TEEM,IDEAL -,TEEN,IDEAS -,TEES,IDENT -,TEFF,IDIOM -,TELE,IDIOT -,TELL,IDLED -,TEMP,IDLER -,TEND,IDLES -,TENS,IDOLS -,TENT,IDYLL -,TERF,IFTAR -,TERM,IGLOO -,TERN,IKONS -,TEST,ILEUM -,TEXT,ILEUS -,THAN,ILIAC -,THAT,ILIUM -,THAW,IMAGE -,THEE,IMAMS -,THEM,IMBED -,THEN,IMBUE -,THEW,IMPEL -,THEY,IMPLY -,THIN,INANE -,THIS,INAPT -,THOU,INBOX -,THRU,INCEL -,THUD,INCUR -,THUG,INCUS -,THUS,INDEX -,TICK,INDIE -,TICS,INEPT -,TIDE,INERT -,TIDY,INFER -,TIED,INFIX -,TIER,INFRA -,TIES,INGOT -,TIFF,INION -,TIFO,INKED -,TIKE,INLAY -,TIKI,INLET -,TILE,INNER -,TILL,INPUT -,TILT,INSET -,TIME,INSPO -,TINE,INTEL -,TING,INTER -,TINS,INTRO -,TINT,INURE -,TINY,IONIC -,TIPI,IOTAS -,TIPS,IRATE -,TIRE,IRKED -,TITS,IRONS -,TIZZ,IRONY -,TOAD,ISLES -,TOBY,ISLET -,TOCK,ISSUE -,TOED,ITCHY -,TOES,ITEMS -,TOFF,IVIED -,TOFU,IVIES -,TOGA,IVORY -,TOGS,IXNAY -,TOIL,JACKS -,TOKE,JADED -,TOLD,JADES -,TOLL,JAFFA -,TOMB,JAGGY -,TOME,JAILS -,TOMS,JAMBS -,TONE,JAMMY -,TONG,JANES -,TONS,JANKY -,TONY,JAPAN -,TOOK,JAPES -,TOOL,JAUNT -,TOOT,JAZZY -,TOPS,JEANS -,TORE,JEEPS -,TORN,JEERS -,TORR,JELLO -,TORS,JELLS -,TORT,JELLY -,TORY,JENNY -,TOSA,JERKS -,TOSH,JERKY -,TOSS,JERRY -,TOTE,JESTS -,TOTS,JETTY -,TOUR,JEWEL -,TOUT,JIBED -,TOWN,JIBES -,TOWS,JIFFS -,TOYS,JIFFY -,TRAM,JIGGY -,TRAP,JILTS -,TRAY,JIMMY -,TREE,JINGO -,TREK,JINKS -,TRIG,JINNS -,TRIM,JIVED -,TRIO,JIVES -,TRIP,JOCKS -,TROD,JOEYS -,TROT,JOHNS -,TRUE,JOINS -,TRUG,JOINT -,TUBA,JOIST -,TUBE,JOKED -,TUBS,JOKER -,TUCK,JOKES -,TUFF,JOKEY -,TUFT,JOLLY -,TUGS,JOLTS -,TUMS,JONES -,TUNA,JOULE -,TUNE,JOUST -,TUNS,JOWLS -,TURD,JUDAS -,TURF,JUDGE -,TURN,JUDGY -,TUSH,JUICE -,TUSK,JUICY -,TUTS,JUKED -,TUTU,JUKES -,TWAT,JULEP -,TWEE,JUMBO -,TWIG,JUMPS -,TWIN,JUMPY -,TWIT,JUNKS -,TWOS,JUNKY -,TYKE,JUNTA -,TYPE,JUNTO -,TYPO,JUROR -,TYRE,JUVIE -,TYRO,KABOB -,UDON,KANJI -,UFOS,KAPOK -,UGLY,KAPPA -,UKES,KAPUT -,ULNA,KARAT -,UMBO,KARMA -,UMPS,KARST -,UNDO,KARTS -,UNIS,KASHA -,UNIT,KATAS -,UNTO,KAYAK -,UPDO,KAZOO -,UPON,KEBAB -,UREA,KECKS -,URGE,KEELS -,URIC,KEENS -,URNS,KEEPS -,USED,KEFIR -,USER,KENDO -,USES,KERBS -,UTES,KETCH -,UVEA,KEYED -,VACS,KHAKI -,VAIL,KICKS -,VAIN,KICKY -,VALE,KIDDO -,VAMP,KIDDY -,VANE,KILLS -,VANS,KILNS -,VAPE,KILOS -,VARY,KILTS -,VASE,KINDA -,VAST,KINDS -,VATS,KINGS -,VEAL,KINKS -,VEEP,KINKY -,VEER,KIOSK -,VEES,KIPPA -,VEIL,KIRKS -,VEIN,KITES -,VELD,KITTY -,VEND,KIVAS -,VENT,KIWIS -,VERB,KLUTZ -,VERT,KNACK -,VERY,KNAVE -,VEST,KNEAD -,VETO,KNEED -,VETS,KNEEL -,VIAL,KNEES -,VIBE,KNELL -,VICE,KNELT -,VIDE,KNIFE -,VIED,KNISH -,VIES,KNITS -,VIEW,KNOBS -,VILE,KNOCK -,VINE,KNOLL -,VINO,KNOTS -,VIOL,KNOWN -,VISA,KNOWS -,VISE,KOALA -,VIVA,KOFTA -,VIVO,KOMBU -,VLOG,KOOKS -,VOID,KOOKY -,VOLE,KORAS -,VOLT,KORMA -,VOTE,KRAUT -,VOWS,KRILL -,WACK,KUDOS -,WADE,KUGEL -,WADI,LABEL -,WADS,LABIA -,WAFT,LABOR -,WAGE,LACED -,WAGS,LACES -,WAIF,LACKS -,WAIL,LADEN -,WAIN,LADLE -,WAIT,LAGER -,WAKE,LAGGY -,WALK,LAHAR -,WALL,LAIRD -,WAND,LAIRS -,WANE,LAITY -,WANS,LAKES -,WANT,LAKSA -,WARD,LAMAS -,WARE,LAMBS -,WARM,LAMER -,WARN,LAMPS -,WARP,LANAI -,WARS,LANCE -,WART,LANDS -,WARY,LANES -,WASH,LANKY -,WASP,LAPEL -,WATT,LAPSE -,WAVE,LARCH -,WAVY,LARDS -,WAXY,LARGE -,WAYS,LARKS -,WEAK,LARVA -,WEAL,LASER -,WEAN,LASSI -,WEAR,LASSO -,WEBS,LASTS -,WEDS,LATCH -,WEED,LATER -,WEEK,LATEX -,WEEP,LATHE -,WEES,LATHS -,WEFT,LATKE -,WEIR,LATTE -,WELD,LAUDS -,WELL,LAUGH -,WELP,LAVER -,WELT,LAWNS -,WENT,LAYER -,WEPT,LAYUP -,WERE,LAZED -,WEST,LAZES -,WETA,LEADS -,WETS,LEAFS -,WHAM,LEAFY -,WHAT,LEAKS -,WHEE,LEAKY -,WHEN,LEANS -,WHET,LEANT -,WHEW,LEAPS -,WHEY,LEAPT -,WHIG,LEARN -,WHIM,LEASE -,WHIN,LEASH -,WHIP,LEAST -,WHIR,LEAVE -,WHIT,LEDES -,WHIZ,LEDGE -,WHOA,LEECH -,WHOM,LEEKS -,WHOP,LEERS -,WHUP,LEERY -,WICK,LEFTS -,WIDE,LEFTY -,WIFE,LEGAL -,WIGS,LEGGY -,WIKI,LEGIT -,WILD,LEMMA -,WILL,LEMON -,WILT,LEMUR -,WILY,LENDS -,WIMP,LENIS -,WIND,LEPER -,WINE,LETCH -,WING,LETUP -,WINK,LEVEE -,WINO,LEVEL -,WINS,LEVER -,WIPE,LEWIS -,WIRE,LEXIS -,WIRY,LIANA -,WISE,LIARS -,WISH,LIBEL -,WISP,LIBRA -,WITH,LICIT -,WITS,LICKS -,WOAD,LIDOS -,WOES,LIEGE -,WOKE,LIENS -,WOKS,LIFER -,WOLD,LIFTS -,WOLF,LIGHT -,WOMB,LIKED -,WONK,LIKEN -,WONT,LIKES -,WOOD,LILAC -,WOOF,LIMBO -,WOOL,LIMBS -,WOOS,LIMED -,WOOT,LIMES -,WOPS,LIMEY -,WORD,LIMIT -,WORE,LIMNS -,WORK,LIMOS -,WORM,LIMPS -,WORN,LINED -,WORT,LINEN -,WOVE,LINER -,WOWS,LINES -,WRAP,LINGO -,WREN,LINKS -,WRIT,LINUX -,WUSS,LIONS -,YACK,LIPID -,YAKS,LIPPY -,YAMS,LISLE -,YANG,LISPS -,YANK,LISTS -,YAPP,LITER -,YAPS,LITHE -,YARD,LITHO -,YARN,LITRE -,YAWN,LIVED -,YAWS,LIVEN -,YEAH,LIVER -,YEAR,LIVES -,YEAS,LIVID -,YECH,LLAMA -,YEET,LOADS -,YELL,LOAFS -,YELP,LOANS -,YEOW,LOATH -,YETI,LOBAR -,YEWS,LOBBY -,YOBS,LOBES -,YOGA,LOCAL -,YOGI,LOCHS -,YOKE,LOCKS -,YOLK,LOCOS -,YOMP,LOCUM -,YOOF,LOCUS -,YORE,LODES -,YOUR,LODGE -,YOUS,LOESS -,YOWL,LOFTS -,YUAN,LOFTY -,YUCK,LOGES -,YULE,LOGIC -,YURT,LOGIN -,YUZU,LOGON -,ZAGS,LOGOS -,ZANY,LOINS -,ZAPS,LOLLS -,ZEAL,LOLLY -,ZEBU,LONER -,ZEDS,LONGS -,ZEES,LOOKS -,ZERO,LOOMS -,ZEST,LOONS -,ZETA,LOONY -,ZHOS,LOOPS -,ZIGS,LOOPY -,ZINC,LOOSE -,ZINE,LOOTS -,ZING,LOPED -,ZIPS,LOPES -,ZITI,LORDS -,ZITS,LORDY -,ZONE,LORIS -,ZONK,LORRY -,ZOOM,LOSER -,ZOOS,LOSES -,ZOUK,LOSSY -,,LOTTO -,,LOTUS -,,LOUGH -,,LOUSE -,,LOUSY -,,LOUTS -,,LOVED -,,LOVER -,,LOVES -,,LOVEY -,,LOWER -,,LOWLY -,,LOYAL -,,LUBED -,,LUBES -,,LUCID -,,LUCKS -,,LUCKY -,,LUCRE -,,LUDIC -,,LUGES -,,LULLS -,,LULUS -,,LUMEN -,,LUMPS -,,LUMPY -,,LUNAR -,,LUNCH -,,LUNGE -,,LUNGS -,,LUPIN -,,LUPUS -,,LURCH -,,LURED -,,LURES -,,LUREX -,,LURGY -,,LURID -,,LURKS -,,LURVE -,,LUSTS -,,LUSTY -,,LUTES -,,LUVVY -,,LYCEE -,,LYCRA -,,LYING -,,LYMPH -,,LYNCH -,,LYRES -,,LYRIC -,,LYSIN -,,LYSIS -,,MACAW -,,MACES -,,MACHE -,,MACHO -,,MACKS -,,MACRO -,,MADAM -,,MADLY -,,MAFIA -,,MAFIC -,,MAGIC -,,MAGMA -,,MAIDS -,,MAILS -,,MAIMS -,,MAINS -,,MAIZE -,,MAJOR -,,MAKER -,,MAKES -,,MALES -,,MALLS -,,MALTS -,,MALTY -,,MAMAS -,,MAMBA -,,MAMBO -,,MAMMA -,,MAMMY -,,MANES -,,MANGA -,,MANGE -,,MANGO -,,MANGY -,,MANIA -,,MANIC -,,MANKY -,,MANLY -,,MANNA -,,MANOR -,,MANSE -,,MANTA -,,MAPLE -,,MARCH -,,MARDY -,,MARES -,,MARIA -,,MARKS -,,MARRY -,,MARSH -,,MARTS -,,MASKS -,,MASON -,,MASTS -,,MATCH -,,MATED -,,MATER -,,MATES -,,MATEY -,,MATHS -,,MATTE -,,MATZO -,,MAULS -,,MAUVE -,,MAVEN -,,MAXED -,,MAXES -,,MAXIM -,,MAXIS -,,MAYBE -,,MAYOR -,,MAYOS -,,MAZES -,,MEALS -,,MEALY -,,MEANS -,,MEANT -,,MEANY -,,MEATS -,,MEATY -,,MEDAL -,,MEDIA -,,MEDIC -,,MEETS -,,MELBA -,,MELDS -,,MELEE -,,MELON -,,MELTS -,,MEMES -,,MEMOS -,,MENDS -,,MENUS -,,MEOWS -,,MERCH -,,MERCS -,,MERCY -,,MERGE -,,MERIT -,,MERRY -,,MESAS -,,MESON -,,MESSY -,,METAL -,,METED -,,METER -,,METES -,,METRE -,,METRO -,,MEWED -,,MEWLS -,,MEZES -,,MEZZE -,,MEZZO -,,MIAOW -,,MICRO -,,MIDDY -,,MIDGE -,,MIDIS -,,MIDST -,,MIENS -,,MIGHT -,,MIKED -,,MIKES -,,MILES -,,MILFS -,,MILKS -,,MILKY -,,MILLS -,,MIMED -,,MIMES -,,MIMIC -,,MINCE -,,MINDS -,,MINED -,,MINER -,,MINES -,,MINGE -,,MINGY -,,MINIM -,,MINIS -,,MINKE -,,MINKS -,,MINOR -,,MINTS -,,MINUS -,,MIRED -,,MIRES -,,MIRIN -,,MIRTH -,,MISER -,,MISSY -,,MISTS -,,MISTY -,,MITER -,,MITES -,,MITRE -,,MITTS -,,MIXED -,,MIXER -,,MIXES -,,MOANS -,,MOATS -,,MOCHA -,,MOCKS -,,MODAL -,,MODEL -,,MODEM -,,MODES -,,MODUS -,,MOGGY -,,MOGUL -,,MOHEL -,,MOIRE -,,MOIST -,,MOLAL -,,MOLAR -,,MOLDS -,,MOLDY -,,MOLES -,,MOLLS -,,MOLTS -,,MOMMA -,,MOMMY -,,MONDE -,,MONEY -,,MONKS -,,MONTE -,,MONTH -,,MOOBS -,,MOOCH -,,MOODS -,,MOODY -,,MOOED -,,MOOKS -,,MOONS -,,MOONY -,,MOORS -,,MOOSE -,,MOOTS -,,MOPED -,,MOPES -,,MORAL -,,MOREL -,,MORES -,,MORON -,,MORPH -,,MORSE -,,MOSEY -,,MOSSY -,,MOTEL -,,MOTES -,,MOTHS -,,MOTIF -,,MOTOR -,,MOTTE -,,MOTTO -,,MOULD -,,MOULT -,,MOUND -,,MOUNT -,,MOURN -,,MOUSE -,,MOUSY -,,MOUTH -,,MOVED -,,MOVER -,,MOVES -,,MOVIE -,,MOWED -,,MOWER -,,MOXIE -,,MUCKS -,,MUCKY -,,MUCUS -,,MUDDY -,,MUFFS -,,MUFTI -,,MUGGY -,,MULCH -,,MULCT -,,MULES -,,MULEY -,,MULGA -,,MULLS -,,MUMMY -,,MUMPS -,,MUMSY -,,MUNCH -,,MUNIS -,,MUONS -,,MURAL -,,MURKY -,,MUSED -,,MUSES -,,MUSHY -,,MUSIC -,,MUSKY -,,MUSOS -,,MUSTS -,,MUSTY -,,MUTED -,,MUTES -,,MUTTS -,,MUZAK -,,MUZZY -,,MYLAR -,,MYNAH -,,MYRRH -,,MYTHS -,,NAANS -,,NABOB -,,NACHO -,,NACRE -,,NADIR -,,NAIAD -,,NAIFS -,,NAILS -,,NAIVE -,,NAKED -,,NAMED -,,NAMES -,,NANAS -,,NANNY -,,NAPES -,,NAPPY -,,NARCS -,,NARIS -,,NARKS -,,NARKY -,,NASAL -,,NASTY -,,NATAL -,,NATCH -,,NATES -,,NATTY -,,NAVAL -,,NAVEL -,,NAVES -,,NAVVY -,,NAZIS -,,NDUJA -,,NEARS -,,NEATH -,,NECKS -,,NEEDS -,,NEEDY -,,NEEPS -,,NEIGH -,,NELLY -,,NERDS -,,NERDY -,,NERVE -,,NERVY -,,NESTS -,,NEVER -,,NEWER -,,NEWLY -,,NEWSY -,,NEWTS -,,NEXUS -,,NICAD -,,NICER -,,NICHE -,,NICKS -,,NIECE -,,NIFFS -,,NIFTY -,,NIGHT -,,NINES -,,NINJA -,,NINNY -,,NINTH -,,NIPPY -,,NIQAB -,,NITES -,,NIXED -,,NIXES -,,NOBLE -,,NOBLY -,,NOCKS -,,NODAL -,,NODES -,,NOHOW -,,NOIRS -,,NOISE -,,NOISY -,,NOMAD -,,NONCE -,,NONES -,,NONIS -,,NOOBS -,,NOOKS -,,NOOKY -,,NOOSE -,,NOPAL -,,NORMS -,,NORTH -,,NOSED -,,NOSES -,,NOSEY -,,NOTCH -,,NOTED -,,NOTES -,,NOUNS -,,NOVAE -,,NOVAS -,,NOVEL -,,NUBBY -,,NUDES -,,NUDGE -,,NUKED -,,NUKES -,,NUMBS -,,NURSE -,,NUTTY -,,NYLON -,,NYMPH -,,OAKEN -,,OAKUM -,,OASES -,,OASIS -,,OATER -,,OATHS -,,OBESE -,,OBEYS -,,OBITS -,,OBOES -,,OCCUR -,,OCEAN -,,OCHER -,,OCHRE -,,OCKER -,,OCTAD -,,OCTAL -,,OCTAN -,,OCTET -,,OCULI -,,ODDER -,,ODDLY -,,ODIUM -,,ODORS -,,ODOUR -,,OFFAL -,,OFFED -,,OFFER -,,OFFIE -,,OFTEN -,,OGIVE -,,OGLED -,,OGLES -,,OGRES -,,OILED -,,OILER -,,OINKS -,,OKAPI -,,OKAYS -,,OLDEN -,,OLDER -,,OLDIE -,,OLIVE -,,OLOGY -,,OMBRE -,,OMEGA -,,OMENS -,,OMITS -,,ONION -,,ONSET -,,OOMPH -,,OOZED -,,OOZES -,,OPALS -,,OPENS -,,OPERA -,,OPINE -,,OPIUM -,,OPTED -,,OPTIC -,,ORACH -,,ORACY -,,ORALS -,,ORBIT -,,ORCAS -,,ORDER -,,ORGAN -,,OTAKU -,,OTHER -,,OTTER -,,OUGHT -,,OUNCE -,,OUSTS -,,OUTDO -,,OUTED -,,OUTER -,,OUTGO -,,OUTRE -,,OUTRO -,,OUTTA -,,OUZOS -,,OVALS -,,OVARY -,,OVATE -,,OVENS -,,OVERS -,,OVERT -,,OVINE -,,OVOID -,,OVULE -,,OWING -,,OWLET -,,OWNED -,,OWNER -,,OXBOW -,,OXIDE -,,OXLIP -,,OXTER -,,OZONE -,,PACED -,,PACES -,,PACEY -,,PACKS -,,PACTS -,,PADDY -,,PADRE -,,PAEAN -,,PAGAN -,,PAGED -,,PAGER -,,PAGES -,,PAILS -,,PAINS -,,PAINT -,,PAIRS -,,PALED -,,PALER -,,PALES -,,PALLS -,,PALLY -,,PALMS -,,PALMY -,,PALSY -,,PANDA -,,PANEL -,,PANES -,,PANGS -,,PANIC -,,PANKO -,,PANSY -,,PANTS -,,PANTY -,,PAPAL -,,PAPAS -,,PAPAW -,,PAPER -,,PAPPY -,,PARCH -,,PARED -,,PARES -,,PARKA -,,PARKS -,,PARKY -,,PARPS -,,PARRY -,,PARSE -,,PARTS -,,PARTY -,,PASEO -,,PASHA -,,PASSE -,,PASTA -,,PASTE -,,PASTS -,,PASTY -,,PATCH -,,PATER -,,PATHS -,,PATIO -,,PATSY -,,PATTY -,,PAUSE -,,PAVED -,,PAVER -,,PAVES -,,PAWED -,,PAWKY -,,PAWNS -,,PAYEE -,,PAYER -,,PEACE -,,PEACH -,,PEAKS -,,PEAKY -,,PEALS -,,PEARL -,,PEARS -,,PEATY -,,PECAN -,,PECKS -,,PEDAL -,,PEDIS -,,PEEKS -,,PEELS -,,PEEPS -,,PEERS -,,PEEVE -,,PEKOE -,,PELTS -,,PENAL -,,PENCE -,,PENIS -,,PENNE -,,PENNY -,,PEONS -,,PEONY -,,PEPPY -,,PERCH -,,PERIL -,,PERKS -,,PERKY -,,PERMS -,,PERPS -,,PERRY -,,PERVS -,,PERVY -,,PESKY -,,PESOS -,,PESTO -,,PESTS -,,PETAL -,,PETER -,,PETIT -,,PETTY -,,PHAGE -,,PHASE -,,PHIAL -,,PHLOX -,,PHONE -,,PHONY -,,PHOTO -,,PHYLA -,,PIANO -,,PICKS -,,PICKY -,,PIECE -,,PIERS -,,PIETY -,,PIGGY -,,PIGMY -,,PIKER -,,PIKES -,,PILAF -,,PILAU -,,PILED -,,PILES -,,PILLS -,,PILOT -,,PIMPS -,,PINCH -,,PINED -,,PINES -,,PINEY -,,PINGS -,,PINKO -,,PINKS -,,PINKY -,,PINNA -,,PINNY -,,PINON -,,PINOT -,,PINTA -,,PINTO -,,PINTS -,,PIOUS -,,PIPAS -,,PIPED -,,PIPER -,,PIPES -,,PIQUE -,,PISCO -,,PISSY -,,PISTE -,,PITAS -,,PITCH -,,PITHS -,,PITHY -,,PITON -,,PITTA -,,PIVOT -,,PIXEL -,,PIXIE -,,PIZZA -,,PLACE -,,PLAID -,,PLAIN -,,PLAIT -,,PLANE -,,PLANK -,,PLANS -,,PLANT -,,PLASH -,,PLASM -,,PLATE -,,PLATS -,,PLAYS -,,PLAZA -,,PLEAD -,,PLEAS -,,PLEAT -,,PLEBS -,,PLICA -,,PLIED -,,PLIER -,,PLIES -,,PLINK -,,PLODS -,,PLONK -,,PLOPS -,,PLOTS -,,PLOWS -,,PLOYS -,,PLUCK -,,PLUGS -,,PLUMB -,,PLUME -,,PLUMP -,,PLUMS -,,PLUMY -,,PLUNK -,,PLUSH -,,POACH -,,POCKS -,,PODGY -,,PODIA -,,POEMS -,,POESY -,,POETS -,,POINT -,,POISE -,,POKED -,,POKER -,,POKES -,,POKEY -,,POLAR -,,POLED -,,POLES -,,POLIO -,,POLIS -,,POLKA -,,POLLS -,,POLYP -,,POLYS -,,POMMY -,,PONCE -,,PONCY -,,PONDS -,,PONGS -,,PONZU -,,POOCH -,,POOED -,,POOHS -,,POOLS -,,POOPS -,,POPES -,,POPPY -,,PORCH -,,PORED -,,PORES -,,PORGY -,,PORKS -,,PORKY -,,PORNO -,,PORNY -,,PORTA -,,PORTS -,,POSED -,,POSER -,,POSES -,,POSEY -,,POSIT -,,POSSE -,,POSTS -,,POTTY -,,POUCH -,,POUFS -,,POULT -,,POUND -,,POURS -,,POUTS -,,POUTY -,,POWER -,,PRAMS -,,PRANG -,,PRANK -,,PRATE -,,PRATS -,,PRAWN -,,PRAYS -,,PREEN -,,PREON -,,PREPS -,,PRESS -,,PREYS -,,PRICE -,,PRICK -,,PRICY -,,PRIDE -,,PRIED -,,PRIES -,,PRIGS -,,PRIME -,,PRIMO -,,PRIMP -,,PRINT -,,PRION -,,PRIOR -,,PRISE -,,PRISM -,,PRIVY -,,PRIZE -,,PROBE -,,PROBS -,,PRODS -,,PROFS -,,PROLE -,,PROMO -,,PROMS -,,PRONE -,,PRONG -,,PRONK -,,PROOF -,,PROPS -,,PROSE -,,PROSY -,,PROTO -,,PROUD -,,PROVE -,,PROWL -,,PROWS -,,PROXY -,,PRUDE -,,PRUNE -,,PSALM -,,PSEUD -,,PSOAS -,,PSYCH -,,PUBES -,,PUBIC -,,PUBIS -,,PUCKS -,,PUDGY -,,PUFFA -,,PUFFS -,,PUFFY -,,PUKED -,,PUKES -,,PUKKA -,,PULLS -,,PULPS -,,PULPY -,,PULSE -,,PUMAS -,,PUMPS -,,PUNCH -,,PUNKS -,,PUNKY -,,PUNNY -,,PUNTS -,,PUNTY -,,PUPAE -,,PUPIL -,,PUPPY -,,PUREE -,,PURER -,,PURGE -,,PURLS -,,PURRS -,,PURSE -,,PUSHY -,,PUSSY -,,PUTTS -,,PUTTY -,,PWNED -,,PYGMY -,,PYLON -,,PYRES -,,PZAZZ -,,QUACK -,,QUADS -,,QUAFF -,,QUAIL -,,QUAKE -,,QUALM -,,QUANT -,,QUARK -,,QUART -,,QUASH -,,QUASI -,,QUAYS -,,QUBIT -,,QUEEN -,,QUEER -,,QUELL -,,QUERN -,,QUERY -,,QUESO -,,QUEST -,,QUEUE -,,QUICK -,,QUIET -,,QUIFF -,,QUILL -,,QUILT -,,QUINT -,,QUIPS -,,QUIRE -,,QUIRK -,,QUITE -,,QUITS -,,QUOLL -,,QUOTA -,,QUOTE -,,QUOTH -,,RABBI -,,RABID -,,RACED -,,RACER -,,RACES -,,RACKS -,,RADAR -,,RADII -,,RADIO -,,RADIX -,,RADON -,,RAFTS -,,RAGAS -,,RAGED -,,RAGES -,,RAGGA -,,RAGUS -,,RAIDS -,,RAILS -,,RAINS -,,RAINY -,,RAISE -,,RAITA -,,RAKED -,,RAKES -,,RALES -,,RALLY -,,RALPH -,,RAMEN -,,RAMPS -,,RAMUS -,,RANCH -,,RANDO -,,RANDY -,,RANGE -,,RANGY -,,RANKS -,,RANTS -,,RAPED -,,RAPES -,,RAPHE -,,RAPID -,,RARER -,,RASPS -,,RASPY -,,RASTA -,,RATED -,,RATES -,,RATIO -,,RATTY -,,RAVED -,,RAVEL -,,RAVEN -,,RAVER -,,RAVES -,,RAYED -,,RAYON -,,RAZED -,,RAZES -,,RAZOR -,,REACH -,,REACT -,,READS -,,READY -,,REALM -,,REALS -,,REAMS -,,REAPS -,,REARM -,,REARS -,,REBAR -,,REBEC -,,REBEL -,,REBID -,,REBUS -,,REBUT -,,RECAP -,,RECCE -,,RECON -,,RECUR -,,RECUT -,,REDID -,,REDOS -,,REDOX -,,REDUX -,,REEDS -,,REEDY -,,REEFS -,,REEKS -,,REELS -,,REEVE -,,REFER -,,REFIT -,,REGAL -,,REHAB -,,REIFY -,,REIGN -,,REIKI -,,REINS -,,REJIG -,,REKEY -,,RELAX -,,RELAY -,,RELIC -,,RELIT -,,RELLO -,,REMIT -,,REMIX -,,RENAL -,,RENDS -,,RENEW -,,RENTS -,,REPAY -,,REPEL -,,REPLY -,,REPOS -,,REPOT -,,RERAN -,,RERUN -,,RESET -,,RESIN -,,RESIT -,,RESTS -,,RETCH -,,RETIE -,,RETRO -,,RETRY -,,REUSE -,,REVEL -,,REVUE -,,REXES -,,RHEUM -,,RHINO -,,RHOMB -,,RHUMB -,,RHYME -,,RIADS -,,RIALS -,,RICED -,,RICER -,,RICIN -,,RICKS -,,RIDER -,,RIDES -,,RIDGE -,,RIFFS -,,RIFLE -,,RIFTS -,,RIGHT -,,RIGID -,,RIGOR -,,RILED -,,RILES -,,RIMES -,,RINDS -,,RINGS -,,RINKS -,,RINSE -,,RIOJA -,,RIOTS -,,RIPEN -,,RIPER -,,RISEN -,,RISER -,,RISES -,,RISKS -,,RISKY -,,RITES -,,RITZY -,,RIVAL -,,RIVEN -,,RIVER -,,RIVET -,,RIYAL -,,ROACH -,,ROADS -,,ROAMS -,,ROANS -,,ROARS -,,ROAST -,,ROBED -,,ROBES -,,ROBIN -,,ROBOT -,,ROCKS -,,ROCKY -,,RODEO -,,ROGER -,,ROGUE -,,ROIDS -,,ROILS -,,ROLES -,,ROLLS -,,ROMAN -,,ROMEO -,,ROMPS -,,RONDO -,,ROOFS -,,ROOKS -,,ROOMS -,,ROOMY -,,ROOST -,,ROOTS -,,ROPED -,,ROPES -,,ROPEY -,,RORTS -,,ROSES -,,ROSIN -,,ROSTI -,,ROTAS -,,ROTIS -,,ROTOR -,,ROUGE -,,ROUGH -,,ROUND -,,ROUSE -,,ROUST -,,ROUTE -,,ROUTS -,,ROVED -,,ROVER -,,ROVES -,,ROWAN -,,ROWDY -,,ROWED -,,ROWER -,,ROYAL -,,RUBES -,,RUBLE -,,RUCHE -,,RUCKS -,,RUDDY -,,RUDER -,,RUFFS -,,RUGBY -,,RUINS -,,RULED -,,RULER -,,RULES -,,RUMBA -,,RUMMY -,,RUMOR -,,RUMPS -,,RUNES -,,RUNGS -,,RUNNY -,,RUNTS -,,RUPEE -,,RURAL -,,RUSES -,,RUSKS -,,RUSTS -,,RUSTY -,,RYOTS -,,SABER -,,SABLE -,,SABRE -,,SACKS -,,SADDO -,,SADHU -,,SADLY -,,SAFER -,,SAFES -,,SAGAS -,,SAGES -,,SAGGY -,,SAILS -,,SAINT -,,SAKES -,,SAKIS -,,SALAD -,,SALES -,,SALLY -,,SALON -,,SALSA -,,SALTS -,,SALTY -,,SALUT -,,SALVE -,,SALVO -,,SAMBA -,,SAMBO -,,SAMEY -,,SANDS -,,SANDY -,,SANER -,,SAPPY -,,SAREE -,,SARGE -,,SARIN -,,SARIS -,,SARKY -,,SASSY -,,SATAY -,,SATED -,,SATIN -,,SATYR -,,SAUCE -,,SAUCY -,,SAUNA -,,SAUTE -,,SAVED -,,SAVER -,,SAVES -,,SAVOR -,,SAVVY -,,SAWED -,,SAXES -,,SAYED -,,SCABS -,,SCADS -,,SCALD -,,SCALE -,,SCALP -,,SCALY -,,SCAMP -,,SCAMS -,,SCANS -,,SCANT -,,SCAPE -,,SCARE -,,SCARF -,,SCARP -,,SCARS -,,SCART -,,SCARY -,,SCATS -,,SCENE -,,SCENT -,,SCHMO -,,SCHWA -,,SCION -,,SCOFF -,,SCOLD -,,SCONE -,,SCOOP -,,SCOOT -,,SCOPE -,,SCORE -,,SCORN -,,SCOTS -,,SCOUR -,,SCOUT -,,SCOWL -,,SCRAM -,,SCRAP -,,SCREE -,,SCREW -,,SCRIM -,,SCRIP -,,SCROD -,,SCRUB -,,SCRUM -,,SCUBA -,,SCUDS -,,SCUFF -,,SCULL -,,SCUMS -,,SCURF -,,SEALS -,,SEAMS -,,SEAMY -,,SEARS -,,SEATS -,,SEBUM -,,SECTS -,,SEDAN -,,SEDER -,,SEDGE -,,SEDUM -,,SEEDS -,,SEEDY -,,SEEKS -,,SEEMS -,,SEEPS -,,SEERS -,,SEGUE -,,SEINE -,,SEIZE -,,SELLS -,,SEMIS -,,SENDS -,,SENSE -,,SEPAL -,,SEPIA -,,SEPOY -,,SEPTA -,,SEPTS -,,SERFS -,,SERGE -,,SERIF -,,SERUM -,,SERVE -,,SERVO -,,SETTS -,,SETUP -,,SEVEN -,,SEVER -,,SEWED -,,SEWER -,,SEXED -,,SEXES -,,SHACK -,,SHADE -,,SHADY -,,SHAFT -,,SHAGS -,,SHAHS -,,SHAKE -,,SHAKY -,,SHALE -,,SHALL -,,SHALT -,,SHAME -,,SHAMS -,,SHANK -,,SHAPE -,,SHARD -,,SHARE -,,SHARK -,,SHARP -,,SHAVE -,,SHAWL -,,SHAWM -,,SHEAF -,,SHEAR -,,SHEDS -,,SHEEN -,,SHEEP -,,SHEER -,,SHEET -,,SHEIK -,,SHELF -,,SHELL -,,SHEWS -,,SHIED -,,SHIES -,,SHIFT -,,SHILL -,,SHIMS -,,SHINE -,,SHINS -,,SHINY -,,SHIPS -,,SHIRE -,,SHIRK -,,SHIRR -,,SHIRT -,,SHISH -,,SHISO -,,SHITE -,,SHITS -,,SHLEP -,,SHOAL -,,SHOCK -,,SHOES -,,SHOJI -,,SHOJO -,,SHONE -,,SHOOK -,,SHOOS -,,SHOOT -,,SHOPS -,,SHORE -,,SHORN -,,SHORT -,,SHOTS -,,SHOUT -,,SHOVE -,,SHOWN -,,SHOWS -,,SHOWY -,,SHRED -,,SHREW -,,SHRUB -,,SHRUG -,,SHTUM -,,SHUCK -,,SHUNS -,,SHUNT -,,SHUSH -,,SHUTS -,,SHYLY -,,SIBYL -,,SICKO -,,SICKS -,,SIDED -,,SIDES -,,SIDLE -,,SIEGE -,,SIEVE -,,SIGHS -,,SIGHT -,,SIGMA -,,SIGNS -,,SILKS -,,SILKY -,,SILLS -,,SILLY -,,SILOS -,,SILTS -,,SIMPS -,,SINCE -,,SINES -,,SINEW -,,SINGE -,,SINGS -,,SINKS -,,SINUS -,,SIRED -,,SIREN -,,SIRES -,,SIRUP -,,SISAL -,,SISSY -,,SITAR -,,SITED -,,SITES -,,SIXER -,,SIXES -,,SIXTH -,,SIXTY -,,SIZED -,,SIZES -,,SKANK -,,SKATE -,,SKEIN -,,SKEWS -,,SKIDS -,,SKIED -,,SKIER -,,SKIES -,,SKIFF -,,SKILL -,,SKIMP -,,SKIMS -,,SKINK -,,SKINS -,,SKINT -,,SKIPS -,,SKIRL -,,SKIRT -,,SKITS -,,SKIVE -,,SKORT -,,SKUAS -,,SKULK -,,SKULL -,,SKUNK -,,SLABS -,,SLACK -,,SLAGS -,,SLAIN -,,SLAKE -,,SLAMS -,,SLANG -,,SLANT -,,SLAPS -,,SLASH -,,SLATE -,,SLATS -,,SLAVE -,,SLAYS -,,SLEBS -,,SLEDS -,,SLEEK -,,SLEEP -,,SLEET -,,SLEPT -,,SLEWS -,,SLICE -,,SLICK -,,SLIDE -,,SLIME -,,SLIMS -,,SLIMY -,,SLING -,,SLINK -,,SLIPS -,,SLITS -,,SLOBS -,,SLOES -,,SLOGS -,,SLOOP -,,SLOPE -,,SLOPS -,,SLOSH -,,SLOTH -,,SLOTS -,,SLOWS -,,SLUGS -,,SLUMP -,,SLUMS -,,SLUNG -,,SLUNK -,,SLURP -,,SLURS -,,SLUSH -,,SLUTS -,,SLYLY -,,SMACK -,,SMALL -,,SMARM -,,SMART -,,SMASH -,,SMEAR -,,SMELL -,,SMELT -,,SMILE -,,SMIRK -,,SMITE -,,SMITH -,,SMOCK -,,SMOGS -,,SMOKE -,,SMOKY -,,SMORE -,,SMOTE -,,SMUTS -,,SNACK -,,SNAFU -,,SNAGS -,,SNAIL -,,SNAKE -,,SNAKY -,,SNAPS -,,SNARE -,,SNARF -,,SNARK -,,SNARL -,,SNEAK -,,SNEER -,,SNICK -,,SNIDE -,,SNIFF -,,SNIPE -,,SNIPS -,,SNITS -,,SNOBS -,,SNOGS -,,SNOOD -,,SNOOK -,,SNOOP -,,SNOOT -,,SNORE -,,SNORT -,,SNOTS -,,SNOUT -,,SNOWS -,,SNOWY -,,SNUBS -,,SNUCK -,,SNUFF -,,SNUGS -,,SOAKS -,,SOAPS -,,SOAPY -,,SOARS -,,SOBER -,,SOCKS -,,SODAS -,,SOFAS -,,SOFTY -,,SOGGY -,,SOILS -,,SOLAR -,,SOLED -,,SOLEI -,,SOLES -,,SOLID -,,SOLOS -,,SOLVE -,,SONAR -,,SONGS -,,SONIC -,,SONNY -,,SOOKS -,,SOOTH -,,SOOTY -,,SOPPY -,,SORER -,,SORES -,,SORRY -,,SORTA -,,SORTS -,,SOUGH -,,SOUKS -,,SOULS -,,SOUND -,,SOUPS -,,SOUPY -,,SOURS -,,SOUSE -,,SOUTH -,,SOWED -,,SOWER -,,SPACE -,,SPACY -,,SPADE -,,SPAKE -,,SPAMS -,,SPANK -,,SPANS -,,SPARE -,,SPARK -,,SPARS -,,SPASM -,,SPATE -,,SPATS -,,SPAWN -,,SPAYS -,,SPAZA -,,SPEAK -,,SPEAR -,,SPECK -,,SPECS -,,SPECT -,,SPEED -,,SPELL -,,SPELT -,,SPEND -,,SPENT -,,SPERM -,,SPEWS -,,SPICA -,,SPICE -,,SPICY -,,SPIED -,,SPIEL -,,SPIES -,,SPIFF -,,SPIKE -,,SPIKY -,,SPILE -,,SPILL -,,SPILT -,,SPINE -,,SPINS -,,SPIRE -,,SPITE -,,SPITS -,,SPITZ -,,SPIVS -,,SPLAT -,,SPLAY -,,SPLIT -,,SPODS -,,SPOIL -,,SPOKE -,,SPOOF -,,SPOOK -,,SPOOL -,,SPOON -,,SPOOR -,,SPORE -,,SPORT -,,SPOTS -,,SPOUT -,,SPRAT -,,SPRAY -,,SPREE -,,SPRIG -,,SPRIT -,,SPROG -,,SPRUE -,,SPUDS -,,SPUME -,,SPUNK -,,SPURN -,,SPURS -,,SPURT -,,SQUAB -,,SQUAD -,,SQUAT -,,SQUIB -,,SQUID -,,STABS -,,STACK -,,STAFF -,,STAGE -,,STAGS -,,STAGY -,,STAID -,,STAIN -,,STAIR -,,STAKE -,,STALE -,,STALK -,,STALL -,,STAMP -,,STAND -,,STANK -,,STAPH -,,STARE -,,STARK -,,STARS -,,START -,,STASH -,,STATE -,,STATS -,,STAVE -,,STAYS -,,STEAD -,,STEAK -,,STEAL -,,STEAM -,,STEED -,,STEEL -,,STEEP -,,STEER -,,STEIN -,,STELE -,,STEMS -,,STENO -,,STENT -,,STEPS -,,STERN -,,STEWS -,,STICK -,,STIES -,,STIFF -,,STILE -,,STILL -,,STILT -,,STING -,,STINK -,,STINT -,,STIRS -,,STOAT -,,STOCK -,,STOEP -,,STOIC -,,STOKE -,,STOLE -,,STOMA -,,STOMP -,,STONE -,,STONY -,,STOOD -,,STOOL -,,STOOP -,,STOPE -,,STOPS -,,STORE -,,STORK -,,STORM -,,STORY -,,STOUP -,,STOUT -,,STOVE -,,STOWS -,,STRAP -,,STRAW -,,STRAY -,,STREP -,,STREW -,,STRIA -,,STRIP -,,STROP -,,STRUM -,,STRUT -,,STUBS -,,STUCK -,,STUDS -,,STUDY -,,STUFF -,,STUMP -,,STUNG -,,STUNK -,,STUNS -,,STUNT -,,STUPA -,,STYES -,,STYLE -,,SUAVE -,,SUCKS -,,SUCKY -,,SUCRE -,,SUEDE -,,SUGAR -,,SUING -,,SUITE -,,SUITS -,,SULCI -,,SULKS -,,SULKY -,,SULLY -,,SUMAC -,,SUMMA -,,SUMPS -,,SUNNY -,,SUNUP -,,SUPER -,,SUPRA -,,SURAL -,,SURER -,,SURFS -,,SURGE -,,SURLY -,,SUSHI -,,SWABS -,,SWAGE -,,SWAGS -,,SWAIN -,,SWALE -,,SWAMI -,,SWAMP -,,SWANK -,,SWAPS -,,SWARD -,,SWARF -,,SWARM -,,SWASH -,,SWATH -,,SWATS -,,SWAYS -,,SWEAR -,,SWEAT -,,SWEDE -,,SWEEP -,,SWEET -,,SWELL -,,SWEPT -,,SWIFT -,,SWIGS -,,SWILL -,,SWIMS -,,SWINE -,,SWING -,,SWIPE -,,SWIRL -,,SWISH -,,SWISS -,,SWIZZ -,,SWOON -,,SWOOP -,,SWOPS -,,SWORD -,,SWORE -,,SWORN -,,SWOTS -,,SWUNG -,,SYNCH -,,SYNCS -,,SYNOD -,,SYNTH -,,SYRUP -,,TABBY -,,TABLA -,,TABLE -,,TABOO -,,TACHE -,,TACIT -,,TACKS -,,TACKY -,,TACOS -,,TAFFY -,,TAIGA -,,TAILS -,,TAINT -,,TAKEN -,,TAKER -,,TAKES -,,TALES -,,TALKS -,,TALKY -,,TALLY -,,TALON -,,TALUS -,,TAMED -,,TAMER -,,TAMES -,,TAMPS -,,TANGO -,,TANGY -,,TANKA -,,TANKS -,,TANSY -,,TAPAS -,,TAPED -,,TAPER -,,TAPES -,,TAPIR -,,TARDY -,,TARNS -,,TAROS -,,TAROT -,,TARPS -,,TARRY -,,TARSI -,,TARTS -,,TARTY -,,TASED -,,TASER -,,TASES -,,TASKS -,,TASTE -,,TASTY -,,TATER -,,TATTS -,,TATTY -,,TAUNT -,,TAUPE -,,TAWNY -,,TAXED -,,TAXES -,,TAXIS -,,TAXON -,,TEACH -,,TEALS -,,TEAMS -,,TEARS -,,TEARY -,,TEASE -,,TEATS -,,TECHS -,,TECHY -,,TEDDY -,,TEEMS -,,TEENS -,,TEENY -,,TEETH -,,TELCO -,,TELEX -,,TELLS -,,TELLY -,,TEMPI -,,TEMPO -,,TEMPS -,,TEMPT -,,TENDS -,,TENET -,,TENON -,,TENOR -,,TENSE -,,TENTH -,,TENTS -,,TEPEE -,,TEPID -,,TERMS -,,TERNS -,,TERRA -,,TERRY -,,TERSE -,,TESLA -,,TESTA -,,TESTS -,,TESTY -,,TEXTS -,,THANE -,,THANG -,,THANK -,,THAWS -,,THEES -,,THEFT -,,THEIR -,,THEME -,,THERE -,,THERM -,,THESE -,,THETA -,,THEWS -,,THICK -,,THIEF -,,THIGH -,,THINE -,,THING -,,THINK -,,THINS -,,THIRD -,,THONG -,,THORN -,,THOSE -,,THREE -,,THREW -,,THROB -,,THROW -,,THRUM -,,THUDS -,,THUGS -,,THUJA -,,THUMB -,,THUMP -,,THUNK -,,THYME -,,TIARA -,,TIBIA -,,TICKS -,,TIDAL -,,TIDED -,,TIDES -,,TIERS -,,TIFFS -,,TIGER -,,TIGHT -,,TIKES -,,TIKKA -,,TILDE -,,TILED -,,TILER -,,TILES -,,TILLS -,,TILTS -,,TIMED -,,TIMER -,,TIMES -,,TIMID -,,TINEA -,,TINES -,,TINGE -,,TINGS -,,TINNY -,,TINTS -,,TIPIS -,,TIPPY -,,TIPSY -,,TIRED -,,TIRES -,,TITAN -,,TITCH -,,TITER -,,TITHE -,,TITLE -,,TITRE -,,TITTY -,,TIZZY -,,TOADS -,,TOADY -,,TOAST -,,TODAY -,,TODDY -,,TOFFS -,,TOGAS -,,TOILS -,,TOKED -,,TOKEN -,,TOKES -,,TOLLS -,,TOMBS -,,TOMES -,,TOMMY -,,TONAL -,,TONED -,,TONER -,,TONES -,,TONGS -,,TONIC -,,TONKS -,,TONNE -,,TOOLS -,,TOOTH -,,TOOTS -,,TOPAZ -,,TOPEE -,,TOPIC -,,TOQUE -,,TORCH -,,TORSO -,,TORTE -,,TORTS -,,TORUS -,,TOTAL -,,TOTED -,,TOTEM -,,TOTES -,,TOTTY -,,TOUCH -,,TOUGH -,,TOURS -,,TOUTS -,,TOWED -,,TOWEL -,,TOWER -,,TOWNS -,,TOXIC -,,TOXIN -,,TOYED -,,TOYON -,,TRACE -,,TRACK -,,TRACT -,,TRADE -,,TRAIL -,,TRAIN -,,TRAIT -,,TRAMP -,,TRAMS -,,TRANQ -,,TRANS -,,TRAPS -,,TRASH -,,TRAWL -,,TRAYS -,,TREAD -,,TREAT -,,TREES -,,TREKS -,,TREND -,,TRESS -,,TREWS -,,TRIAD -,,TRIAL -,,TRIBE -,,TRICE -,,TRICK -,,TRIDE -,,TRIED -,,TRIER -,,TRIES -,,TRIKE -,,TRILL -,,TRIMS -,,TRIOS -,,TRIPE -,,TRIPS -,,TRITE -,,TROLL -,,TROMP -,,TRONC -,,TROOP -,,TROPE -,,TROTH -,,TROTS -,,TROUT -,,TROVE -,,TRUCE -,,TRUCK -,,TRUER -,,TRUGS -,,TRULY -,,TRUMP -,,TRUNK -,,TRUSS -,,TRUST -,,TRUTH -,,TRYST -,,TSARS -,,TUBAL -,,TUBAS -,,TUBBY -,,TUBED -,,TUBER -,,TUBES -,,TUCKS -,,TUFTS -,,TULES -,,TULIP -,,TULLE -,,TUMID -,,TUMMY -,,TUMOR -,,TUNAS -,,TUNED -,,TUNER -,,TUNES -,,TUNIC -,,TUPLE -,,TUQUE -,,TURBO -,,TURDS -,,TURFS -,,TURNS -,,TURPS -,,TUSKS -,,TUTOR -,,TUTTI -,,TUTUS -,,TWAIN -,,TWANG -,,TWATS -,,TWEAK -,,TWEED -,,TWEEN -,,TWEET -,,TWERK -,,TWERP -,,TWICE -,,TWIGS -,,TWILL -,,TWINE -,,TWINK -,,TWINS -,,TWIRL -,,TWIRP -,,TWIST -,,TWIXT -,,TYING -,,TYKES -,,TYPED -,,TYPES -,,TYPOS -,,TYRES -,,TYROS -,,TZARS -,,UDDER -,,ULCER -,,ULNAE -,,ULNAS -,,ULTRA -,,UMAMI -,,UMBEL -,,UMBER -,,UMBOS -,,UMBRA -,,UNBAN -,,UNBOX -,,UNCAP -,,UNCLE -,,UNCUT -,,UNDER -,,UNDID -,,UNDUE -,,UNFED -,,UNFIT -,,UNHIP -,,UNIFY -,,UNION -,,UNITE -,,UNITS -,,UNITY -,,UNLIT -,,UNMET -,,UNSEE -,,UNSET -,,UNTIE -,,UNTIL -,,UNWED -,,UNZIP -,,UPDOS -,,UPEND -,,UPLIT -,,UPPED -,,UPPER -,,UPSET -,,URBAN -,,URGED -,,URGES -,,URINE -,,USAGE -,,USERS -,,USHER -,,USING -,,USUAL -,,USURP -,,USURY -,,UTERI -,,UTTER -,,UVEAS -,,UVULA -,,VACAY -,,VAGAL -,,VAGUE -,,VALES -,,VALET -,,VALID -,,VALOR -,,VALSE -,,VALUE -,,VALVE -,,VAMPS -,,VANES -,,VAPED -,,VAPER -,,VAPES -,,VAPID -,,VAPOR -,,VARIX -,,VARUS -,,VASES -,,VAULT -,,VAUNT -,,VEENA -,,VEEPS -,,VEERS -,,VEGAN -,,VEGES -,,VEILS -,,VEINS -,,VELAR -,,VELDT -,,VELUM -,,VENAL -,,VENDS -,,VENOM -,,VENTS -,,VENUE -,,VERBS -,,VERGE -,,VERSE -,,VERSO -,,VERTS -,,VERVE -,,VESTA -,,VESTS -,,VETCH -,,VEXED -,,VEXES -,,VIALS -,,VIBED -,,VIBES -,,VICAR -,,VICES -,,VIDEO -,,VIEWS -,,VIGIL -,,VIGOR -,,VILER -,,VILLA -,,VILLI -,,VINES -,,VINYL -,,VIOLA -,,VIOLS -,,VIPER -,,VIRAL -,,VIRUS -,,VISAS -,,VISES -,,VISIT -,,VISOR -,,VISTA -,,VITAL -,,VITRO -,,VIVAS -,,VIVID -,,VIXEN -,,VLOGS -,,VOCAL -,,VODKA -,,VOGUE -,,VOICE -,,VOIDS -,,VOILA -,,VOILE -,,VOLAR -,,VOLES -,,VOLTE -,,VOLTS -,,VOMER -,,VOMIT -,,VOTED -,,VOTER -,,VOTES -,,VOUCH -,,VOWED -,,VOWEL -,,VROOM -,,VULVA -,,VYING -,,WACKO -,,WACKY -,,WADDY -,,WADED -,,WADER -,,WADES -,,WADIS -,,WAFER -,,WAFTS -,,WAGED -,,WAGER -,,WAGES -,,WAGON -,,WAGYU -,,WAHOO -,,WAIFS -,,WAILS -,,WAINS -,,WAIST -,,WAITS -,,WAIVE -,,WAKED -,,WAKEN -,,WAKES -,,WALKS -,,WALLS -,,WALLY -,,WALTZ -,,WANDS -,,WANED -,,WANES -,,WANTS -,,WARDS -,,WARES -,,WARMS -,,WARNS -,,WARPS -,,WARTS -,,WARTY -,,WASHY -,,WASPS -,,WASPY -,,WASTE -,,WATCH -,,WATER -,,WATTS -,,WAVED -,,WAVER -,,WAVES -,,WAXED -,,WAXEN -,,WAXES -,,WAZOO -,,WEALS -,,WEARS -,,WEARY -,,WEAVE -,,WEDGE -,,WEEDS -,,WEEDY -,,WEEKS -,,WEENY -,,WEEPS -,,WEEPY -,,WEIGH -,,WEIRD -,,WELCH -,,WELDS -,,WELLS -,,WELLY -,,WELSH -,,WELTS -,,WENCH -,,WETAS -,,WETLY -,,WHACK -,,WHALE -,,WHANG -,,WHARF -,,WHEAT -,,WHEEL -,,WHELK -,,WHELM -,,WHELP -,,WHERE -,,WHETS -,,WHICH -,,WHIFF -,,WHILE -,,WHIMS -,,WHINE -,,WHINY -,,WHIPS -,,WHIRL -,,WHIRR -,,WHIRS -,,WHISK -,,WHIST -,,WHITE -,,WHIZZ -,,WHOLE -,,WHOMP -,,WHOOP -,,WHOPS -,,WHORE -,,WHORL -,,WHOSE -,,WHUPS -,,WICKS -,,WIDEN -,,WIDER -,,WIDES -,,WIDOW -,,WIDTH -,,WIELD -,,WIGHT -,,WIKIS -,,WILCO -,,WILES -,,WILLS -,,WILLY -,,WILTS -,,WIMPS -,,WIMPY -,,WINCE -,,WINCH -,,WINDS -,,WINDY -,,WINED -,,WINES -,,WINGS -,,WINKS -,,WINOS -,,WIPED -,,WIPER -,,WIPES -,,WIRED -,,WIRES -,,WISED -,,WISER -,,WISES -,,WISPS -,,WISPY -,,WITCH -,,WITTY -,,WIVES -,,WODGE -,,WOKEN -,,WOLDS -,,WOLFS -,,WOMAN -,,WOMBS -,,WOMEN -,,WONGA -,,WONKS -,,WONKY -,,WOODS -,,WOODY -,,WOOED -,,WOOFS -,,WOOLY -,,WOOZY -,,WORDS -,,WORDY -,,WORKS -,,WORLD -,,WORMS -,,WORMY -,,WORRY -,,WORSE -,,WORST -,,WORTH -,,WOULD -,,WOUND -,,WOVEN -,,WOWED -,,WRACK -,,WRAPS -,,WRATH -,,WREAK -,,WRECK -,,WRENS -,,WREST -,,WRING -,,WRIST -,,WRITE -,,WRITS -,,WRONG -,,WROTE -,,WRUNG -,,WRYLY -,,WURST -,,WUSHU -,,XENON -,,XEROX -,,XYLEM -,,YACHT -,,YAHOO -,,YAKKA -,,YANKS -,,YAPPY -,,YARDS -,,YARNS -,,YAWED -,,YAWNS -,,YEARN -,,YEARS -,,YEAST -,,YECCH -,,YELLS -,,YELPS -,,YERBA -,,YESES -,,YETIS -,,YIELD -,,YIKES -,,YOBBO -,,YODEL -,,YOGIS -,,YOKED -,,YOKEL -,,YOKES -,,YOLKS -,,YOMPS -,,YONKS -,,YOUNG -,,YOURS -,,YOUSE -,,YOUTH -,,YOWLS -,,YUCCA -,,YUCKY -,,YUKKY -,,YUMMY -,,YUPPY -,,YURTS -,,YUZUS -,,ZAKAT -,,ZAPPY -,,ZEBRA -,,ZEROS -,,ZETAS -,,ZILCH -,,ZINES -,,ZIPPY -,,ZLOTY -,,ZONAL -,,ZONED -,,ZONES +3_letter,4_letter,5_letter +ABA,ABET,ABACK +ABS,ABLE,ABAFT +ACE,ABLY,ABASE +ACT,ABUT,ABASH +ADD,ACAI,ABATE +ADO,ACED,ABAYA +ADS,ACER,ABBEY +ADZ,ACES,ABBOT +AFT,ACHE,ABEAM +AGA,ACHY,ABETS +AGE,ACID,ABHOR +AGO,ACME,ABIDE +AHA,ACNE,ABLED +AHI,ACRE,ABLER +AID,ACTS,ABODE +AIL,ADDS,ABORT +AIM,ADDY,ABOUT +AIR,ADZE,ABOVE +AKA,AEON,ABUSE +ALA,AERO,ABUTS +ALE,AFAR,ABUZZ +ALL,AFRO,ABYSS +ALP,AGAR,ACERS +ALS,AGAS,ACHED +ALT,AGED,ACHES +ALU,AGES,ACIDS +AMP,AGIN,ACING +ANA,AGIO,ACINI +AND,AGOG,ACKEE +ANT,AGUE,ACORN +ANY,AHEM,ACRES +APE,AHIS,ACRID +APP,AHOY,ACTED +APT,AIDE,ACTIN +ARC,AIDS,ACTOR +ARE,AILS,ACUTE +ARK,AIMS,ADAGE +ARM,AIRS,ADAPT +ART,AIRY,ADDED +ASH,AJAR,ADDER +ASK,AKIN,ADDLE +ASP,ALAS,ADEPT +ASS,ALEC,ADIEU +ATE,ALES,ADIOS +AVE,ALGA,ADMAN +AWE,ALLY,ADMEN +AWL,ALMA,ADMIN +AXE,ALMS,ADMIT +AYE,ALOE,ADOBE +BAA,ALOO,ADOBO +BAD,ALSO,ADOPT +BAE,ALTO,ADORE +BAG,ALUM,ADORN +BAH,AMEN,ADULT +BAM,AMID,ADZES +BAN,AMMO,AEGIS +BAO,AMPS,AEONS +BAP,AMYL,AERIE +BAR,ANAL,AFFIX +BAS,ANCE,AFIRE +BAT,ANEW,AFOOT +BAY,ANKH,AFORE +BED,ANON,AFOUL +BEE,ANTE,AFROS +BEG,ANTI,AFTER +BEN,ANTS,AGAIN +BET,ANUS,AGAPE +BIB,APED,AGATE +BID,APES,AGAVE +BIG,APEX,AGENT +BIN,APPS,AGGRO +BIO,APSE,AGILE +BIT,AQUA,AGING +BIZ,ARCH,AGISM +BOA,ARCO,AGIST +BOB,ARCS,AGITA +BOD,AREA,AGLOW +BOG,ARES,AGONY +BOO,ARIA,AGORA +BOP,ARID,AGREE +BOT,ARKS,AHEAD +BOW,ARMS,AHOLD +BOX,ARMY,AIDED +BOY,ARSE,AIDES +BRA,ARTS,AILED +BRO,ARTY,AIMED +BUB,ASHY,AIOLI +BUD,ASKS,AIRED +BUG,ASPS,AIRER +BUM,ATOM,AISLE +BUN,ATOP,AITCH +BUS,AULD,ALACK +BUT,AUNT,ALARM +BUY,AURA,ALBUM +BYE,AUTO,ALDER +CAB,AVER,ALECS +CAD,AVID,ALERT +CAL,AVOW,ALGAE +CAM,AWAY,ALIAS +CAN,AWED,ALIBI +CAP,AWES,ALIEN +CAR,AWLS,ALIGN +CAT,AWOL,ALIKE +CAW,AWRY,ALIVE +CAY,AXED,ALKIE +CHA,AXEL,ALKYL +CHI,AXES,ALLAY +CIG,AXIL,ALLEY +CIS,AXIS,ALLOT +COB,AXLE,ALLOW +COD,AXON,ALLOY +COG,AYES,ALOES +COL,BAAS,ALOFT +CON,BABA,ALOHA +COO,BABE,ALONE +COP,BABY,ALONG +COR,BACK,ALOOF +COS,BADE,ALOUD +COT,BAES,ALPHA +COW,BAGS,ALTAR +COX,BAHT,ALTER +COY,BAIL,ALTOS +CRY,BAIT,ALUMS +CUB,BAKE,ALWAY +CUD,BALD,AMASS +CUP,BALE,AMAZE +CUR,BALK,AMBER +CUT,BALL,AMBIT +CUZ,BALM,AMBLE +DAB,BAND,AMEBA +DAD,BANE,AMEND +DAG,BANG,AMENS +DAH,BANK,AMIDE +DAL,BANS,AMIGO +DAM,BAPS,AMINE +DAP,BARB,AMINO +DAW,BARD,AMISS +DAY,BARE,AMITY +DEF,BARF,AMNIO +DEL,BARK,AMONG +DEN,BARM,AMOUR +DEW,BARN,AMPED +DEX,BARS,AMPLE +DID,BASE,AMPLY +DIE,BASH,AMUCK +DIG,BASK,AMUSE +DIM,BASS,ANCHO +DIN,BATH,ANGEL +DIP,BATS,ANGER +DIS,BAUD,ANGLE +DOC,BAWL,ANGLO +DOE,BAYS,ANGRY +DOG,BEAD,ANGST +DOH,BEAK,ANIME +DOL,BEAM,ANION +DOM,BEAN,ANISE +DON,BEAR,ANKLE +DOO,BEAT,ANNAL +DOS,BEAU,ANNEX +DOT,BECK,ANNOY +DOX,BEDS,ANNUL +DRY,BEEF,ANODE +DUB,BEEN,ANTED +DUD,BEEP,ANTES +DUE,BEER,ANTIC +DUG,BEES,ANTIS +DUH,BEET,ANTSY +DUN,BEGS,ANVIL +DUO,BELL,AORTA +DUX,BELT,APACE +DYE,BEND,APART +DZO,BENS,APHID +EAR,BENT,APING +EAT,BERG,APNEA +EAU,BERM,APPAL +EBB,BEST,APPLE +ECO,BETA,APPLY +EEK,BETS,APRES +EEL,BEVY,APRON +EFF,BIAS,APSES +EFT,BIBB,APTLY +EGG,BIBS,ARBOR +EGO,BIDE,ARCED +EKE,BIDS,ARCUS +ELF,BIER,ARDOR +ELK,BIFF,AREAS +ELM,BIGS,ARECA +EMO,BIKE,ARENA +EMU,BILE,ARETE +END,BILK,ARGAN +EON,BILL,ARGON +ERA,BIND,ARGOT +ERG,BING,ARGUE +ERM,BINS,ARIAS +ERR,BIOS,ARISE +ESS,BIRD,ARMED +EST,BIRL,ARMOR +ETA,BIRO,AROID +EVE,BITE,AROMA +EWE,BITS,AROSE +EYE,BLAB,ARRAY +FAB,BLAG,ARROW +FAD,BLAH,ARSED +FAG,BLED,ARSES +FAM,BLEW,ARSON +FAN,BLIP,ARTIC +FAR,BLOB,ARTSY +FAT,BLOC,ASANA +FAV,BLOG,ASCOT +FAX,BLOT,ASHEN +FED,BLOW,ASHES +FEE,BLUB,ASIDE +FEM,BLUE,ASKED +FEN,BLUR,ASKEW +FEW,BOAR,ASPEN +FEY,BOAS,ASPIC +FEZ,BOAT,ASSAY +FIB,BOBS,ASSES +FIE,BODE,ASSET +FIG,BODY,ASTER +FIN,BOGS,ASTIR +FIR,BOGY,ATLAS +FIT,BOHO,ATOLL +FIX,BOIL,ATOMS +FLU,BOLD,ATONE +FLY,BOLE,ATRIA +FOB,BOLL,ATTIC +FOE,BOLO,AUDIO +FOG,BOLT,AUDIT +FOP,BOMB,AUGER +FOR,BONA,AUGHT +FOX,BOND,AUGUR +FRO,BONE,AUNTS +FRY,BONG,AUNTY +FUG,BONK,AURAL +FUN,BONY,AURAS +FUR,BOOB,AUTOS +GAB,BOOK,AUXIN +GAD,BOOM,AVAIL +GAG,BOON,AVANT +GAL,BOOR,AVERS +GAP,BOOS,AVERT +GAR,BOOT,AVIAN +GAS,BOPS,AVOID +GAT,BORE,AVOWS +GAY,BORN,AWAIT +GED,BOSH,AWAKE +GEE,BOSS,AWARD +GEL,BOTH,AWARE +GEM,BOTS,AWASH +GEN,BOUT,AWAYS +GEO,BOWL,AWFUL +GET,BOWS,AWING +GIB,BOXY,AWOKE +GIF,BOYS,AXIAL +GIG,BOZO,AXILS +GIN,BRAD,AXING +GIT,BRAG,AXIOM +GNU,BRAN,AXION +GOB,BRAS,AXLES +GOD,BRAT,AXONS +GOO,BRAY,AZURE +GOT,BRED,BAAED +GRR,BREW,BABAS +GUM,BRIE,BABEL +GUN,BRIG,BABES +GUT,BRIM,BACCY +GUV,BRIO,BACKS +GUY,BRIS,BACON +GYM,BRIT,BADDY +GYP,BROS,BADGE +HAD,BROW,BADLY +HAG,BRUH,BAGEL +HAH,BRUT,BAGGY +HAM,BUBO,BAILS +HAS,BUCK,BAIRN +HAT,BUDS,BAITS +HAW,BUFF,BAIZE +HAY,BUGS,BAKED +HEM,BULB,BAKER +HEN,BULK,BAKES +HER,BULL,BALDY +HEW,BUMF,BALED +HEX,BUMP,BALER +HEY,BUMS,BALES +HID,BUNG,BALKS +HIE,BUNK,BALKY +HIM,BUNS,BALLS +HIP,BUNT,BALLY +HIS,BUOY,BALMS +HIT,BURL,BALMY +HMM,BURN,BALSA +HOB,BURP,BALTI +HOC,BURR,BANAL +HOD,BURS,BANDS +HOE,BURY,BANDY +HOG,BUSH,BANGS +HON,BUSK,BANJO +HOP,BUST,BANKS +HOS,BUSY,BANNS +HOT,BUTE,BANTS +HOW,BUTS,BARBS +HUB,BUTT,BARDS +HUE,BUYS,BARED +HUG,BUZZ,BARER +HUH,BYES,BARES +HUM,BYRE,BARFS +HUN,BYTE,BARGE +HUP,CABS,BARKS +HUT,CAFE,BARMY +ICE,CAFF,BARNS +ICK,CAGE,BARON +ICY,CAKE,BARRE +IDS,CALF,BASAL +IFS,CALL,BASED +ILK,CALM,BASER +ILL,CALS,BASES +IMP,CAME,BASIC +INK,CAMI,BASIL +INN,CAMO,BASIN +INS,CAMP,BASIS +ION,CAMS,BASKS +IRE,CANE,BASSO +IRK,CANS,BASTE +ISH,CANT,BATCH +ISM,CAPE,BATED +ISO,CAPO,BATHE +ITS,CAPS,BATHS +IVY,CARB,BATIK +JAB,CARD,BATON +JAG,CARE,BATTY +JAM,CARK,BAULK +JAR,CARP,BAWDY +JAW,CARS,BAWLS +JAY,CART,BAYED +JET,CASE,BAYOU +JIB,CASH,BEACH +JIG,CASK,BEADS +JOB,CAST,BEADY +JOE,CATS,BEAKS +JOG,CAUL,BEAMS +JOT,CAVA,BEAMY +JOY,CAVE,BEANO +JUG,CAWS,BEANS +JUS,CAYS,BEARD +JUT,CEDE,BEARS +KEG,CELL,BEAST +KEN,CELT,BEATS +KEY,CENT,BEAUS +KID,CERT,BEAUT +KIN,CHAD,BEAUX +KIP,CHAI,BEBOP +KIR,CHAP,BECKS +KIT,CHAR,BEECH +KOI,CHAT,BEEFS +LAB,CHAV,BEEFY +LAD,CHEF,BEEPS +LAG,CHEM,BEERS +LAH,CHEW,BEERY +LAM,CHIA,BEETS +LAP,CHIC,BEFIT +LAT,CHIN,BEGAN +LAV,CHIP,BEGAT +LAW,CHIS,BEGET +LAX,CHIT,BEGIN +LAY,CHOC,BEGOT +LEA,CHOP,BEGUN +LED,CHOW,BEIGE +LEE,CHUB,BEING +LEG,CHUG,BELAY +LEI,CHUM,BELCH +LET,CIAO,BELIE +LEY,CIGS,BELLE +LID,CIST,BELLS +LIE,CITE,BELLY +LIP,CITY,BELOW +LIS,CLAD,BELTS +LIT,CLAG,BENCH +LOB,CLAM,BENDS +LOG,CLAN,BENDY +LOO,CLAP,BENNE +LOP,CLAW,BENNY +LOT,CLAY,BENTO +LOW,CLEF,BERET +LOX,CLIP,BERGS +LUG,CLIT,BERKS +LUX,CLOD,BERMS +LYE,CLOG,BERRY +MAC,CLOP,BERTH +MAD,CLOT,BESET +MAG,CLOY,BESOM +MAL,CLUB,BESTS +MAM,CLUE,BETAS +MAN,COAL,BETEL +MAP,COAT,BEVEL +MAR,COAX,BEVVY +MAT,COBB,BEZEL +MAW,COBS,BHAJI +MAX,COCA,BIALY +MAY,COCK,BIBLE +MED,COCO,BICEP +MEG,CODA,BIDDY +MEH,CODE,BIDED +MEN,COED,BIDET +MET,COGS,BIERS +MEW,COHO,BIFFS +MIC,COIL,BIGHT +MID,COIN,BIGLY +MIL,COIR,BIGOT +MIX,COKE,BIJOU +MOB,COLA,BIKED +MOD,COLD,BIKER +MOM,COLT,BIKES +MON,COMA,BIKIE +MOO,COMB,BILBY +MOP,COME,BILGE +MOS,COMP,BILKS +MOW,CONE,BILLS +MUD,CONK,BILLY +MUG,CONS,BIMBO +MUM,CONY,BINDI +MUS,COOK,BINDS +NAB,COOL,BINGE +NAG,COON,BINGO +NAH,COOP,BINGS +NAN,COOS,BIOME +NAP,COOT,BIOTA +NAT,COPE,BIPED +NAV,COPS,BIRCH +NAY,COPY,BIRDS +NEE,CORD,BIRLS +NET,CORE,BIROS +NEW,CORK,BIRTH +NIB,CORM,BISON +NIL,CORN,BITCH +NIP,CORS,BITER +NIT,COSH,BITES +NIX,COST,BITTY +NOB,COSY,BIVVY +NOD,COTE,BLABS +NOG,COTS,BLACK +NOH,COUP,BLADE +NON,COVE,BLAGS +NOR,COWL,BLAHS +NOS,COWS,BLAME +NOT,COXA,BLAND +NOW,COZY,BLANK +NTH,CRAB,BLARE +NUN,CRAG,BLASE +NUT,CRAM,BLAST +OAF,CRAP,BLAZE +OAK,CRAY,BLEAK +OAR,CRED,BLEAT +OAT,CREW,BLEED +OBO,CRIB,BLEEP +ODD,CRIT,BLEND +ODE,CROC,BLESS +ODS,CROP,BLEST +OFF,CROW,BLIMP +OFT,CRUD,BLIND +OHM,CRUX,BLING +OHS,CUBE,BLINK +OIK,CUBS,BLIPS +OIL,CUES,BLISS +OLD,CUFF,BLITZ +OLE,CUKE,BLOAT +ONE,CULL,BLOBS +ONO,CULT,BLOCK +OOF,CUPS,BLOCS +OOH,CURB,BLOGS +OPS,CURD,BLOKE +OPT,CURE,BLOND +ORA,CURL,BLOOD +ORB,CURS,BLOOM +ORC,CURT,BLOOP +ORE,CUSP,BLOTS +ORT,CUSS,BLOWN +OUD,CUTE,BLOWS +OUR,CUTS,BLOWY +OUT,CYAN,BLUBS +OVA,CYST,BLUER +OWE,CZAR,BLUES +OWL,DABS,BLUEY +OWN,DADA,BLUFF +OWT,DADO,BLUNT +PAD,DADS,BLURB +PAL,DAFT,BLURS +PAN,DAGS,BLURT +PAP,DAIS,BLUSH +PAR,DALE,BOARD +PAS,DAME,BOARS +PAT,DAMN,BOAST +PAW,DAMP,BOATS +PAX,DAMS,BOBBY +PAY,DANG,BOCCE +PEA,DANK,BODED +PEC,DANS,BODES +PEE,DAPS,BODGE +PEG,DARE,BOGAN +PEN,DARK,BOGEY +PEP,DARN,BOGGY +PER,DART,BOGIE +PES,DASH,BOGUS +PET,DATA,BOHEA +PEW,DATE,BOHOS +PHI,DAUB,BOILS +PHO,DAWN,BOING +PIC,DAYS,BOINK +PIE,DAZE,BOKEH +PIG,DEAD,BOLES +PIN,DEAF,BOLLS +PIP,DEAL,BOLTS +PIS,DEAN,BOLUS +PIT,DEAR,BOMBE +PIX,DEBT,BOMBS +PLY,DECK,BONCE +POD,DECO,BONDS +POI,DEED,BONED +POM,DEEM,BONER +POO,DEEP,BONES +POP,DEER,BONGO +POT,DEFO,BONGS +POW,DEFT,BONKS +POX,DEFY,BONNY +PRE,DEKE,BONUS +PRO,DELI,BOOBS +PRY,DELL,BOOBY +PSI,DELS,BOOED +PUB,DEMO,BOOKS +PUD,DENS,BOOMS +PUG,DENT,BOOMY +PUN,DENY,BOONS +PUP,DESI,BOORS +PUS,DESK,BOOST +PUT,DEUS,BOOTH +PWN,DEWY,BOOTS +QAT,DHAL,BOOTY +QUA,DHOW,BOOZE +RAD,DIAL,BOOZY +RAG,DIBS,BOPPY +RAH,DICE,BORAX +RAM,,BORED +RAN,DIED,BORER +RAP,DIES,BORES +RAT,DIET,BORNE +RAW,DIFF,BORON +RAY,DIGS,BOSKY +REC,DIKE,BOSOM +RED,DILL,BOSON +REF,DIME,BOSSY +REG,DIMS,BOSUN +REM,DINE,BOTCH +REP,DING,BOTHY +RES,DINK,BOTOX +REV,DINO,BOUGH +REX,DINS,BOULE +REZ,DINT,BOUND +RHO,DIPS,BOURN +RIB,DIRE,BOUTS +RID,DIRK,BOVID +RIG,DIRT,BOWED +RIM,DISC,BOWEL +RIP,DISH,BOWER +ROB,DISK,BOWLS +ROD,DISS,BOXED +ROE,DITZ,BOXER +ROO,DIVA,BOXES +ROT,DIVE,BOXTY +ROW,DIVS,BOZOS +RUB,DOBS,BRAAI +RUE,DOCK,BRACE +RUG,DOCS,BRACT +RUM,DODO,BRAGS +RUN,DOER,BRAID +RUT,DOES,BRAIN +RYE,DOFF,BRAKE +SAC,DOGE,BRAND +SAD,DOGS,BRASH +SAG,DOJO,BRASS +SAP,DOLE,BRATS +SAT,DOLL,BRAVE +SAW,DOLT,BRAVO +SAX,DOME,BRAWL +SAY,DONE,BRAWN +SEA,DONG,BRAYS +SEC,DONS,BREAD +SEE,DOOM,BREAK +SET,DOOR,BREAM +SEW,DOPE,BREED +SEX,DORK,BRENT +SHE,DORM,BREVE +SHH,DORY,BREWS +SHY,DOSA,BRIAR +SIB,DOSE,BRIBE +SIC,DOSH,BRICK +SIM,DOSS,BRIDE +SIN,DOST,BRIEF +SIP,DOTE,BRIER +SIR,DOTH,BRIGS +SIS,DOTS,BRILL +SIT,DOUR,BRIMS +SIX,DOVE,BRINE +SKA,DOWN,BRING +SKI,DOXX,BRINK +SKY,DOZE,BRINY +SLY,DOZY,BRISK +SOB,DRAB,BRITS +SOD,DRAG,BROAD +SOL,DRAM,BROCH +SON,DRAT,BROCK +SOP,DRAW,BROIL +SOS,DRAY,BROKE +SOT,DREW,BRONC +SOU,DREY,BROOD +SOW,DRIP,BROOK +SOX,DROP,BROOM +SOY,DRUB,BROTH +SOZ,DRUG,BROWN +SPA,DRUM,BROWS +SPY,DUAL,BRUIT +STY,DUBS,BRUME +SUB,DUCK,BRUNT +SUE,DUCT,BRUSH +SUM,DUDE,BRUTE +SUN,DUDS,BUCKS +SUP,DUEL,BUDDY +SUS,DUES,BUDGE +TAB,DUET,BUFFS +TAD,DUFF,BUGGY +TAG,DUKE,BUGLE +TAJ,DULL,BUILD +TAM,DULY,BUILT +TAN,DUMB,BULBS +TAP,DUMP,BULGE +TAR,DUNE,BULKS +TAS,DUNG,BULKY +TAT,DUNK,BULLA +TAU,DUNS,BULLS +TAX,DUOS,BULLY +TEA,DUPE,BUMPH +TED,DURA,BUMPS +TEE,DUSK,BUMPY +TEL,DUST,BUNCH +TEN,DUTY,BUNDT +THE,DYAD,BUNGS +THO,DYED,BUNKS +THY,DYER,BUNNY +TIC,DYES,BUNTS +TIE,DYKE,BUOYS +TIN,DZOS,BURET +TIP,EACH,BURKA +TIS,EARL,BURLS +TIT,EARN,BURLY +TOD,EARS,BURNS +TOE,EASE,BURNT +TOG,EAST,BURPS +TOM,EASY,BURQA +TON,EATS,BURRS +TOO,EAVE,BURSA +TOP,EBBS,BURST +TOR,ECHO,BUSBY +TOT,EDDY,BUSED +TOW,EDGE,BUSES +TOY,EDGY,BUSHY +TRY,EDIT,BUSKS +TUB,EELS,BUSTS +TUG,EFFS,BUSTY +TUM,EFTS,BUTCH +TUN,EGAD,BUTTE +TUT,EGGS,BUTTS +TUX,EGGY,BUTTY +TWO,EGOS,BUXOM +UFO,EKED,BUYER +UGH,EKES,BUZZY +UMM,ELAN,BYLAW +UMP,ELKS,BYRES +UNI,ELMS,BYTES +UPS,ELSE,BYWAY +URN,EMIC,CABAL +USE,EMIR,CABBY +UTE,EMIT,CABER +VAC,EMOS,CABIN +VAN,EMUS,CABLE +VAR,ENDS,CACAO +VAT,ENVY,CACHE +VAX,EONS,CACTI +VEE,EPEE,CADDY +VEG,EPIC,CADET +VET,EPOS,CADGE +VEX,ERAS,CADRE +VIA,ERGO,CAFES +VIE,ERGS,CAFFS +VIM,ERRS,CAGED +VIS,ETAS,CAGES +VOG,ETCH,CAGEY +VOW,ETIC,CAIRN +VOX,EURO,CAJUN +WAD,EVEN,CAKED +WAG,EVER,CAKES +WAN,EVES,CALLA +WAR,EVIL,CALLS +WAS,EWER,CALMS +WAT,EWES,CALVE +WAX,EXAM,CALYX +WAY,EXES,CAMAS +WEB,EXIT,CAMEL +WED,EXPO,CAMEO +WEE,EYED,CAMPS +WET,EYES,CAMPY +WHO,FABS,CANAL +WHY,FACE,CANDY +WIG,FACT,CANED +WIN,FADE,CANES +WIT,FADO,CANID +WIZ,FADS,CANNA +WOE,FAFF,CANNY +WOK,FAGS,CANOE +WON,FAIL,CANON +WOO,FAIN,CANTO +WOP,FAIR,CAPED +WOT,FAKE,CAPER +WOW,FALL,CAPES +WRY,FAME,CAPEX +YAH,FAMS,CAPON +YAK,FANG,CAPOS +YAM,FANS,CAPUT +YAP,FARE,CARAT +YAW,FARL,CARBS +YAY,FARM,CARDI +YEA,FART,CARDS +YEH,FAST,CARDY +YEN,FATE,CARED +YEP,FATS,CARER +YER,FAUN,CARES +YES,FAUX,CARET +YET,FAVA,CARGO +YEW,FAVE,CAROB +YIN,FAVS,CAROL +YIP,FAWN,CAROM +YOB,FAZE,CARPI +YON,FEAR,CARPS +YOU,FEAT,CARRY +YUK,FECK,CARTE +YUM,FEDS,CARTS +YUP,FEED,CARVE +ZAG,FEEL,CASED +ZAP,FEES,CASES +ZED,FEET,CASKS +ZEE,FELL,CASTE +ZEN,FELT,CASTS +ZIG,FEND,CATCH +ZIP,FENS,CATER +ZIT,FERN,CATTY +ZOO,FESS,CAULK +ZZZ,FEST,CAULS +,FETA,CAUSE +,FETE,CAVED +,FEUD,CAVES +,FIAT,CAVIL +,FIBS,CAWED +,FIEF,CEASE +,FIFE,CECUM +,FIGS,CEDAR +,FILE,CEDED +,FILL,CEDES +,FILM,CELEB +,FILO,CELLO +,FIND,CELLS +,FINE,CENTS +,FINK,CERTS +,FINS,CHADS +,FIRE,CHAFE +,FIRM,CHAFF +,FIRN,CHAIN +,FIRS,CHAIR +,FISC,CHALK +,FISH,CHAMP +,FIST,CHANT +,FITS,CHAOS +,FIVE,CHAPS +,FIZZ,CHARD +,FLAB,CHARM +,FLAG,CHARS +,FLAK,CHART +,FLAN,CHARY +,FLAP,CHASE +,FLAT,CHASM +,FLAW,CHATS +,FLAX,CHAVS +,FLAY,CHEAP +,FLEA,CHEAT +,FLED,CHECK +,FLEE,CHEEK +,FLEW,CHEEP +,FLEX,CHEER +,FLIP,CHEFS +,FLIT,CHEMO +,FLOE,CHESS +,FLOG,CHEST +,FLOP,CHEWS +,FLOW,CHEWY +,FLUB,CHICK +,FLUE,CHIDE +,FLUX,CHIEF +,FOAL,CHILD +,FOAM,CHILE +,FOBS,CHILI +,FOCI,CHILL +,FOES,CHIME +,FOGS,CHIMP +,FOGY,CHINA +,FOHN,CHINE +,FOIL,CHINK +,FOLD,CHINO +,FOLK,CHINS +,FOND,CHIPS +,FONT,CHIRP +,FOOD,CHITS +,FOOL,CHIVE +,FOOT,CHOCK +,FOPS,CHOCS +,FORA,CHOIR +,FORD,CHOKE +,FORE,CHOMP +,FORK,CHOPS +,FORM,CHORD +,FORT,CHORE +,FOUL,CHOSE +,FOUR,CHOWS +,FOWL,CHUCK +,FOXY,CHUFF +,FRAT,CHUGS +,FRAY,CHUMP +,FREE,CHUMS +,FRET,CHUNK +,FRIT,CHURN +,FROG,CHURR +,FROM,CHUTE +,FUEL,CHYME +,FUGU,CIDER +,FULL,CIGAR +,FUME,CIGGY +,FUND,CILIA +,FUNK,CILLS +,FURL,CINCH +,FURS,CIRCA +,FURY,CISCO +,FUSE,CISSY +,FUSS,CISTS +,FUTZ,CITED +,FUZE,CITES +,FUZZ,CIVET +,GABS,CIVIC +,GADS,CIVIL +,GAFF,CIVVY +,GAGA,CLACK +,GAGE,CLADE +,GAGS,CLADS +,GAIN,CLAIM +,GAIT,CLAMP +,GALA,CLAMS +,GALE,CLANG +,GALL,CLANK +,GALS,CLANS +,GAME,CLAPS +,GAMY,CLARY +,GANG,CLASH +,GAOL,CLASP +,GAPE,CLASS +,GAPS,CLAWS +,GARB,CLEAN +,GASH,CLEAR +,GASP,CLEAT +,GATE,CLEFS +,GATS,CLEFT +,GAVE,CLERK +,GAWD,CLICK +,GAWK,CLIFF +,GAWP,CLIMB +,GAYS,CLIME +,GAZE,CLINE +,GEAR,CLING +,GEDS,CLINK +,GEEK,CLIPS +,GEES,CLITS +,GEEZ,CLOAK +,GELD,CLOCK +,GELS,CLODS +,GEMS,CLOGS +,GENE,CLOMP +,GENS,CLONE +,GENT,CLOPS +,GERM,CLOSE +,GETS,CLOTH +,GHAT,CLOTS +,GHEE,CLOUD +,GIBE,CLOUT +,GIFT,CLOVE +,GIGA,CLOWN +,GIGS,CLOYS +,GILD,CLUBS +,GILL,CLUCK +,GILT,CLUED +,GIMP,CLUES +,GINS,CLUMP +,GIRD,CLUNG +,GIRL,CLUNK +,GIRO,COACH +,GIRT,COALS +,GIST,COAST +,GITE,COATI +,GITS,COATS +,GIVE,COBRA +,GLAD,COCKS +,GLAM,COCKY +,GLEE,COCOA +,GLEN,CODAS +,GLIA,CODEC +,GLIB,CODED +,GLOB,CODER +,GLOM,CODES +,GLOP,CODEX +,GLOW,CODON +,GLUE,COEDS +,GLUG,COIGN +,GLUM,COILS +,GLUT,COINS +,GNAT,COKES +,GNAW,COLAS +,GNUS,COLDS +,GOAD,COLEY +,GOAL,COLIC +,GOAT,COLON +,GOBO,COLOR +,GOBS,COLTS +,GODS,COMAS +,GOER,COMBE +,GOES,COMBI +,GOLD,COMBO +,GOLF,COMBS +,GONE,COMER +,GONG,COMES +,GOOD,COMET +,GOOF,COMFY +,GOON,COMIC +,GOOP,COMMA +,GORE,COMMS +,GORP,COMPS +,GORY,CONCH +,GOSH,CONDO +,GOTH,CONED +,GOUT,CONES +,GOWN,CONEY +,GRAB,CONGA +,GRAD,CONIC +,GRAM,CONKS +,GRAN,CONVO +,GRAY,COOED +,GREW,COOKS +,GREY,COOLS +,GRID,COONS +,GRIM,COOPS +,GRIN,COOTS +,GRIP,COPED +,GRIT,COPES +,GROG,COPRA +,GROK,COPSE +,GROW,CORAL +,GRUB,CORDS +,GUAC,CORED +,GUFF,CORES +,GULF,CORGI +,GULL,CORKS +,GULP,CORKY +,GUMS,CORMS +,GUNK,CORNS +,GUNS,CORNY +,GURU,CORPS +,GUSH,COSTA +,GUST,COSTS +,GUTS,COTTA +,GUYS,COUCH +,GYMS,COUGH +,GYRE,COULD +,GYRI,COUNT +,GYRO,COUPE +,HAAR,COUPS +,HACK,COURT +,HADJ,COUTH +,HAFT,COVED +,HAGS,COVEN +,HAHA,COVER +,HAIL,COVES +,HAIR,COVET +,HAJI,COVEY +,HAJJ,COVID +,HAKA,COWED +,HAKE,COWER +,HALE,COWLS +,HALF,COWRY +,HALL,COXED +,HALO,COXES +,HALT,COYER +,HAMS,COYLY +,HAND,COYPU +,HANG,COZEN +,HANK,CRABS +,HARD,CRACK +,HARE,CRAFT +,HARK,CRAGS +,HARM,CRAIC +,HARP,CRAMP +,HART,CRAMS +,HASH,CRANE +,HASP,CRANK +,HAST,CRAPE +,HATE,CRAPS +,HATH,CRASH +,HATS,CRASS +,HAUL,CRATE +,HAVE,CRAVE +,HAWK,CRAWL +,HAYS,CRAYS +,HAZE,CRAZE +,HAZY,CRAZY +,HEAD,CREAK +,HEAL,CREAM +,HEAP,CREDO +,HEAR,CREED +,HEAT,CREEK +,HECK,CREEP +,HEED,CREME +,HEEL,CREPE +,HEFT,CREPT +,HEIR,CRESS +,HELD,CREST +,HELL,CREWS +,HELM,CRIBS +,HELP,CRICK +,HEMP,CRIED +,HEMS,CRIER +,HENS,CRIES +,HERB,CRIME +,HERD,CRIMP +,HERE,CRISP +,HERO,CROAK +,HERS,CROCK +,HEWN,CROCS +,HEWS,CROFT +,HICK,CRONE +,HIDE,CRONY +,HIED,CROOK +,HIES,CROON +,HIGH,CROPS +,HIKE,CRORE +,HILL,CROSS +,HILT,CROUP +,HIND,CROWD +,HINT,CROWN +,HIPS,CROWS +,HIRE,CRUDE +,HISS,CRUEL +,HITS,CRUET +,HIVE,CRUMB +,HIYA,CRUMP +,HMMM,CRUNK +,HOAX,CRUSH +,HOBO,CRUST +,HOBS,CRYPT +,HOCK,CUBBY +,HODS,CUBED +,HOED,CUBES +,HOES,CUBIC +,HOGS,CUBIT +,HOKI,CUFFS +,HOLD,CUKES +,HOLE,CULLS +,HOLM,CULPA +,HOLS,CULTS +,HOLT,CUMIN +,HOLY,CUPPA +,HOME,CURBS +,HOMO,CURDS +,HONE,CURED +,HONK,CURES +,HOOD,CURIE +,HOOF,CURIO +,HOOK,CURLS +,HOOP,CURLY +,HOOT,CURRY +,HOPE,CURSE +,HOPS,CURVE +,HORN,CURVY +,HOSE,CUSHY +,HOST,CUTER +,HOTS,CUTEY +,HOUR,CUTIE +,HOVE,CUTIS +,HOWL,CUTUP +,HOYA,CWTCH +,HUBS,CYBER +,HUED,CYCAD +,HUES,CYCLE +,HUFF,CYCLO +,HUGE,CYDER +,HUGS,CYNIC +,HULA,CYSTS +,HULK,CZARS +,HULL,DADDY +,HUMP,DADOS +,HUMS,DAFFY +,HUNG,DAGGY +,HUNK,DAILY +,HUNT,DAIRY +,HURL,DAISY +,HURT,DALES +,HUSH,DALLY +,HUSK,DAMES +,HUTS,DAMNS +,HYMN,DAMPS +,HYPE,DANCE +,HYPO,DANDY +,IAMB,DARED +,IBEX,DARES +,IBIS,DARNS +,ICED,DARTS +,ICES,DATED +,ICKY,DATES +,ICON,DATUM +,IDEA,DAUBE +,IDEM,DAUBS +,IDES,DAUNT +,IDLE,DAWNS +,IDLY,DAZED +,IDOL,DEALS +,IDYL,DEALT +,IFFY,DEANS +,IKAT,DEARS +,IKON,DEATH +,ILEX,DEBAR +,ILIA,DEBIT +,ILLS,DEBTS +,IMAM,DEBUG +,IMPS,DEBUT +,INCH,DECAF +,INFO,DECAL +,INKS,DECAY +,INKY,DECKS +,INNS,DECOR +,INTO,DECOY +,IONS,DECRY +,IOTA,DEEDS +,IRIS,DEEMS +,IRKS,DEETS +,IRON,DEFAT +,ISLE,DEFER +,ISMS,DEFFO +,ITCH,DEIFY +,ITEM,DEIGN +,JABS,DEISM +,JACK,DEITY +,JADE,DEKED +,JAGS,DEKES +,JAIL,DELAY +,JAMB,DELIS +,JAMS,DELLS +,JANE,DELTA +,JAPE,DELTS +,JARS,DELVE +,JAVA,DEMON +,JAWS,DEMOS +,JAYS,DEMUR +,JAZZ,DENIM +,JEAN,DENSE +,JEEP,DENTS +,JEER,DEPOT +,JEEZ,DEPTH +,JELL,DERBY +,JERK,DERMA +,JEST,DESIS +,JETS,DESKS +,JIBE,DETER +,JIBS,DETOX +,JIFF,DEUCE +,JIGS,DEVIL +,JILT,DEVON +,JINN,DHOTI +,JINX,DIALS +,JIVE,DIARY +,JIZZ,DICED +,JOBS,DICES +,JOCK,DICEY +,JOES, +,JOEY,DICKY +,JOGS,DICOT +,JOHN,DICTA +,JOIN,DIDDY +,JOKE,DIETS +,JOLT,DIGIT +,JOSH,DIKES +,JOSS,DILDO +,JOTS,DILLY +,JOWL,DIMES +,JOYS,DIMLY +,JUDO,DINAR +,JUGS,DINED +,JUJU,DINER +,JUKE,DINES +,JUMP,DINGO +,JUNK,DINGS +,JURY,DINGY +,JUST,DINKS +,JUTE,DINKY +,JUTS,DINTS +,KALE,DIODE +,KART,DIPPY +,KATA,DIPSO +,KEEL,DIRGE +,KEEN,DIRKS +,KEEP,DIRTY +,KEGS,DISCO +,KELP,DISCS +,KENS,DISHY +,KEPT,DISKS +,KERB,DITCH +,KEYS,DITSY +,KHAN,DITTO +,KHAT,DITTY +,KICK,DITZY +,KIDS,DIVAN +,KILL,DIVAS +,KILN,DIVED +,KILO,DIVER +,KILT,DIVES +,KIND,DIVIS +,KING,DIVOT +,KINK,DIVVY +,KIPS,DIZZY +,KIRK,DJINN +,KIRS,DOBRO +,KISS,DOCKS +,KITE,DODGE +,KITH,DODGY +,KITS,DODOS +,KIVA,DOERS +,KIWI,DOEST +,KNEE,DOETH +,KNEW,DOFFS +,KNIT,DOGGO +,KNOB,DOGGY +,KNOT,DOGIE +,KNOW,DOGMA +,KOOK,DOILY +,KRIS,DOING +,LABS,DOJOS +,LACE,DOLED +,LACK,DOLES +,LACY,DOLLS +,LADS,DOLLY +,LADY,DOLMA +,LAGS,DOLTS +,LAID,DOMED +,LAIN,DOMES +,LAIR,DONEE +,LAKE,DONGA +,LAKH,DONGS +,LAMA,DONNA +,LAMB,DONOR +,LAME,DONUT +,LAMP,DOOMS +,LAND,DOOMY +,LANE,DOONA +,LANK,DOORS +,LAPS,DOOZY +,LARD,DOPES +,LARK,DOPEY +,LARP,DORKS +,LASH,DORKY +,LASS,DORMS +,LAST,DORSA +,LATE,DOSAS +,LATH,DOSES +,LATS,DOTED +,LAUD,DOTES +,LAVA,DOTTY +,LAVS,DOUBT +,LAWN,DOUCE +,LAWS,DOUGH +,LAYS,DOULA +,LAZE,DOUSE +,LAZY,DOVES +,LEAD,DOWDY +,LEAF,DOWEL +,LEAK,DOWER +,LEAN,DOWNS +,LEAP,DOWNY +,LEAS,DOWRY +,LECH,DOWSE +,LEDE,DOXED +,LEEK,DOXES +,LEER,DOYEN +,LEET,DOZED +,LEFT,DOZEN +,LEGS,DOZER +,LEIS,DOZES +,LEND,DRAFT +,LENS,DRAGS +,LENT,DRAIN +,LESS,DRAKE +,LEST,DRAMA +,LETS,DRAMS +,LEVY,DRANK +,LEWD,DRAPE +,LIAR,DRAWL +,LIBS,DRAWN +,LICE,DRAWS +,LICK,DRAYS +,LIDO,DREAD +,LIDS,DREAM +,LIED,DRECK +,LIEF,DREGS +,LIEN,DRESS +,LIES,DREYS +,LIEU,DRIBS +,LIFE,DRIED +,LIFT,DRIER +,LIKE,DRIES +,LILT,DRIFT +,LILY,DRILL +,LIMA,DRILY +,LIMB,DRINK +,LIME,DRIPS +,LIMN,DRIVE +,LIMO,DROID +,LIMP,DROIT +,LINE,DROLL +,LING,DROME +,LINK,DRONE +,LINO,DROOL +,LINT,DROOP +,LION,DROPS +,LIPO,DROSS +,LIPS,DROVE +,LIRA,DROWN +,LISP,DRUGS +,LIST,DRUID +,LITE,DRUMS +,LIVE,DRUNK +,LOAD,DRUPE +,LOAF,DRYAD +,LOAN,DRYER +,LOBE,DRYLY +,LOBS,DUALS +,LOCH,DUCAL +,LOCI,DUCHY +,LOCK,DUCKS +,LOCO,DUCKY +,LODE,DUCTS +,LOFT,DUDES +,LOGE,DUELS +,LOGO,DUETS +,LOGS,DUFFS +,LOGY,DUKES +,LOIN,DULLS +,LOLL,DULLY +,LOLZ,DULSE +,LOME,DUMBS +,LONE,DUMMY +,LONG,DUMPS +,LOOK,DUMPY +,LOOM,DUNCE +,LOON,DUNES +,LOOP,DUNNO +,LOOS,DUNNY +,LOOT,DUPED +,LOPE,DUPES +,LOPS,DUPLE +,LORD,DURRY +,LORE,DURUM +,LORN,DUSKY +,LOSE,DUSTS +,LOSS,DUSTY +,LOST,DUTCH +,LOTH,DUVET +,LOTS,DUXES +,LOUD,DWARF +,LOUT,DWEEB +,LOVE,DWELL +,LOWS,DWELT +,LUBE,DYADS +,LUCK,DYERS +,LUGE,DYING +,LUGS,DYKES +,LULL,EAGER +,LULU,EAGLE +,LUMP,EARED +,LUNG,EARLS +,LURE,EARLY +,LURK,EARNS +,LUSH,EARTH +,LUST,EASED +,LUTE,EASEL +,LUXE,EASES +,LYES,EATEN +,LYME,EATER +,LYNX,EAVES +,LYRE,EBBED +,MACE,EBONY +,MACK,EBOOK +,MACS,ECLAT +,MADE,EDEMA +,MAGE,EDGED +,MAGI,EDGES +,MAGS,EDICT +,MAID,EDIFY +,MAIL,EDITS +,MAIM,EDUCE +,MAIN,EEJIT +,MAKE,EERIE +,MALA,EFFED +,MALE,EGADS +,MALL,EGGED +,MALT,EGRET +,MAMA,EIGHT +,MAMS,EJECT +,MANE,EKING +,MANO,ELATE +,MANS,ELBOW +,MANY,ELDER +,MAPS,ELECT +,MARE,ELEGY +,MARK,ELFIN +,MARL,ELIDE +,MART,ELITE +,MASA,ELOPE +,MASH,ELUDE +,MASK,ELVEN +,MASS,ELVER +,MAST,ELVES +,MATE,EMAIL +,MATH,EMBED +,MATS,EMBER +,MATT,EMCEE +,MAUL,EMEND +,MAWS,EMERY +,MAXI,EMIRS +,MAYO,EMITS +,MAYS,EMMER +,MAZE,EMMET +,MAZY,EMOJI +,MEAD,EMOTE +,MEAL,EMPTY +,MEAN,ENACT +,MEAT,ENDED +,MEDS,ENDOW +,MEEK,ENEMA +,MEET,ENEMY +,MEGA,ENJOY +,MEGS,ENNUI +,MELD,ENOKI +,MELT,ENROL +,MEME,ENSUE +,MEMO,ENTER +,MEND,ENTRY +,MENU,ENVOY +,MEOW,EPEES +,MERC,EPICS +,MERE,EPOCH +,MESA,EPOXY +,MESH,EQUAL +,MESS,EQUID +,META,EQUIP +,METE,ERASE +,METH,ERECT +,MEWL,ERHUS +,MEWS,ERODE +,MEZE,ERRED +,MICA,ERROR +,MICE,ERUPT +,MICS,ESKER +,MIDI,ESSAY +,MIEN,ESTER +,MIKE,ETHER +,MILD,ETHIC +,MILE,ETHOS +,MILF,ETHYL +,MILK,ETUDE +,MILL,ETYMA +,MILO,EUROS +,MIME,EVADE +,MIND,EVENS +,MINE,EVENT +,MINI,EVERT +,MINK,EVERY +,MINT,EVICT +,MINX,EVILS +,MIRE,EVOKE +,MIRY,EWERS +,MISO,EXACT +,MISS,EXALT +,MIST,EXAMS +,MITE,EXCEL +,MITT,EXECS +,MOAN,EXERT +,MOAT,EXILE +,MOBS,EXIST +,MOCK,EXITS +,MODE,EXPAT +,MODS,EXPEL +,MOJO,EXPOS +,MOLD,EXTOL +,MOLE,EXTRA +,MOLL,EXUDE +,MOLT,EXULT +,MOLY,EXURB +,MOMS,EYING +,MONK,EYRIE +,MONO,EZINE +,MOOD,FABLE +,MOOK,FACED +,MOON,FACES +,MOOR,FACET +,MOOS,FACIA +,MOOT,FACTS +,MOPE,FADDY +,MOPS,FADED +,MORE,FADES +,MORN,FAFFS +,MOSH,FAGOT +,MOSS,FAILS +,MOST,FAINT +,MOTE,FAIRS +,MOTH,FAIRY +,MOVE,FAITH +,MOWN,FAKED +,MOWS,FAKER +,MUCH,FAKES +,MUCK,FAKIR +,MUFF,FALLS +,MUGS,FALSE +,MULE,FAMED +,MULL,FAMES +,MUMS,FANCY +,MUNG,FANGS +,MUNI,FANNY +,MUON,FAQIR +,MURK,FARAD +,MUSE,FARCE +,MUSH,FARED +,MUSK,FARES +,MUSS,FARLS +,MUST,FARMS +,MUTE,FARRO +,MUTT,FARTS +,MWAH,FASTS +,MYTH,FATAL +,NAAN,FATED +,NABS,FATES +,NADS,FATSO +,NAFF,FATTY +,NAGS,FATWA +,NAIL,FAULT +,NAME,FAUNA +,NANA,FAUNS +,NANO,FAVES +,NANS,FAVOR +,NAPE,FAWNS +,NAPS,FAXED +,NARC,FAXES +,NARK,FAYRE +,NARY,FAZED +,NATS,FAZES +,NAVE,FEARS +,NAVY,FEAST +,NAYS,FEATS +,NEAP,FECAL +,NEAR,FECES +,NEAT,FEEDS +,NECK,FEELS +,NEED,FEIGN +,NEEP,FEINT +,NEGS,FELID +,NEON,FELLA +,NERD,FELLS +,NESS,FELON +,NEST,FELTS +,NETS,FEMME +,NEWS,FEMUR +,NEWT,FENCE +,NEXT,FENDS +,NIBS,FERAL +,NICE,FERNS +,NICK,FERRY +,NIGH,FESTS +,NINE,FETAL +,NIPS,FETCH +,NITE,FETED +,NITS,FETES +,NOBS,FETID +,NOCK,FETUS +,NODE,FEUDS +,NODS,FEVER +,NOEL,FEWER +,NOES,FIATS +,NOIR,FIBER +,NOMS,FIBRE +,NONE,FIBRO +,NONI,FICHE +,NOOB,FICUS +,NOOK,FIEFS +,NOON,FIELD +,NOPE,FIEND +,NORI,FIERY +,NORM,FIFTH +,NOSE,FIFTY +,NOSH,FIGGY +,NOSY,FIGHT +,NOTE,FILCH +,NOUN,FILED +,NOUS,FILES +,NOVA,FILET +,NOWT,FILLS +,NUDE,FILLY +,NUKE,FILMS +,NUMB,FILMY +,NUNS,FILTH +,NUTS,FINAL +,OAFS,FINCH +,OAKS,FINDS +,OARS,FINED +,OATH,FINER +,OATS,FINES +,OBEY,FINKS +,OBIT,FINNY +,OBOE,FIORD +,OBVS,FIRED +,ODDS,FIRES +,ODES,FIRMS +,ODOR,FIRST +,OFFS,FIRTH +,OGLE,FISHY +,OGRE,FISKS +,OHMS,FISTS +,OIKS,FITCH +,OILS,FITLY +,OILY,FIVER +,OINK,FIVES +,OKAY,FIXED +,OLDE,FIXER +,OMEN,FIXES +,OMIT,FIZZY +,ONCE,FJORD +,ONES,FLACK +,ONLY,FLAGS +,ONTO,FLAIL +,ONUS,FLAIR +,ONYX,FLAKE +,OOPS,FLAKY +,OOZE,FLAME +,OOZY,FLAMS +,OPAL,FLANK +,OPEN,FLANS +,OPUS,FLAPS +,ORAL,FLARE +,ORBS,FLASH +,ORCS,FLASK +,ORES,FLATS +,ORGY,FLAWS +,OURS,FLAYS +,OUST,FLEAS +,OUTS,FLECK +,OVAL,FLEEK +,OVEN,FLEES +,OVER,FLEET +,OWED,FLESH +,OWES,FLICK +,OWLS,FLIER +,OWNS,FLIES +,OXEN,FLING +,PACE,FLINT +,PACK,FLIPS +,PACT,FLIRT +,PADS,FLITS +,PAGE,FLOAT +,PAID,FLOCK +,PAIL,FLOES +,PAIN,FLOGS +,PAIR,FLOOD +,PALE,FLOOR +,PALL,FLOPS +,PALM,FLORA +,PALS,FLOSS +,PANE,FLOUR +,PANG,FLOUT +,PANS,FLOWN +,PANT,FLOWS +,PAPA,FLUBS +,PAPS,FLUES +,PARA,FLUFF +,PARE,FLUID +,PARK,FLUKE +,PARS,FLUKY +,PART,FLUME +,PASS,FLUNG +,PAST,FLUNK +,PATE,FLUSH +,PATH,FLUTE +,PATS,FLYBY +,PAVE,FLYER +,PAWN,FOALS +,PAWS,FOAMS +,PAYS,FOAMY +,PEAK,FOCAL +,PEAL,FOCUS +,PEAR,FOEHN +,PEAS,FOGEY +,PEAT,FOGGY +,PECK,FOILS +,PECS,FOIST +,PEED,FOLDS +,PEEL,FOLIO +,PEEP,FOLKS +,PEER,FOLLY +,PEGS,FONTS +,PELT,FOODS +,PENS,FOODY +,PENT,FOOLS +,PERK,FOOTS +,PERT,FOOTY +,PESO,FORAY +,PEST,FORCE +,PETS,FORDS +,PEWS,FORES +,PHIS,FOREX +,PICK,FORGE +,PIER,FORGO +,PIES,FORKS +,PIGS,FORMS +,PIKE,FORTE +,PILE,FORTH +,PILL,FORTS +,PIMP,FORTY +,PINE,FORUM +,PINK,FOSSA +,PINS,FOULS +,PINT,FOUND +,PIPE,FOUNT +,PISS,FOURS +,PITH,FOVEA +,PITS,FOWLS +,PITY,FOXED +,PLAN,FOXES +,PLAT,FOYER +,PLAY,FRACK +,PLEA,FRAIL +,PLOD,FRAME +,PLOT,FRANC +,PLOW,FRANK +,PLOY,FRASS +,PLUG,FRATS +,PLUM,FRAUD +,PLUS,FRAYS +,PODS,FREAK +,POEM,FREED +,POET,FREER +,POIS,FREES +,POKE,FRESH +,POLE,FRETS +,POLL,FRIAR +,POLO,FRIED +,POMP,FRIER +,POND,FRIES +,PONS,FRILL +,PONY,FRISK +,POOH,FRITZ +,POOL,FRIZZ +,POOR,FROCK +,POPE,FROGS +,POPS,FROND +,PORE,FRONT +,PORK,FROSH +,PORT,FROST +,POSE,FROTH +,POST,FROWN +,POTS,FROZE +,POUR,FRUIT +,POUT,FRUMP +,PRAM,FRYER +,PRAY,FUBSY +,PREP,FUDGE +,PREY,FUELS +,PRIG,FUGAL +,PRIM,FUGGY +,PROD,FUGLY +,PROM,FUGUE +,PROP,FULLY +,PROS,FUMED +,PROW,FUMES +,PSIS,FUMET +,PSST,FUNDI +,PUBE,FUNDS +,PUBS,FUNGI +,PUCE,FUNGO +,PUCK,FUNKY +,PUDS,FUNNY +,PUFF,FURLS +,PUGS,FUROR +,PUKE,FURRY +,PULI,FURZE +,PULL,FUSED +,PULP,FUSES +,PUMA,FUSSY +,PUMP,FUSTY +,PUNK,FUTON +,PUNS,FUZZY +,PUNT,GABBY +,PUNY,GABLE +,PUPA,GAFFE +,PUPS,GAFFS +,PURE,GAGED +,PURI,GAGES +,PURL,GAILY +,PURR,GAINS +,PUSH,GAITS +,PUSS,GALAH +,PUTS,GALAS +,PUTT,GALES +,PUTZ,GALLS +,PYRE,GAMED +,QUAD,GAMER +,QUAY,GAMES +,QUID,GAMMA +,QUIM,GAMMY +,QUIP,GAMUT +,QUIT,GANGS +,QUIZ,GANJA +,RACE,GAOLS +,RACK,GAPED +,RACY,GAPES +,RAFT,GARBS +,RAGA,GARDA +,RAGE,GASES +,RAGS,GASPS +,RAGU,GASSY +,RAID,GATED +,RAIL,GATES +,RAIN,GATOR +,RAKE,GAUDY +,RALE,GAUGE +,RAMI,GAUNT +,RAMP,GAUZE +,RAMS,GAUZY +,RAND,GAVEL +,RANG,GAWKS +,RANI,GAWKY +,RANK,GAWPS +,RANT,GAYER +,RAPE,GAYLY +,RAPS,GAZED +,RAPT,GAZER +,RARE,GAZES +,RASH,GEARS +,RASP,GECKO +,RATA,GEEKS +,RATE,GEEKY +,RATS,GEESE +,RAVE,GELDS +,RAWS,GENES +,RAYS,GENIC +,RAZE,GENIE +,READ,GENII +,REAL,GENOA +,REAM,GENRE +,REAP,GENTS +,REAR,GENUS +,RECS,GEOID +,REDO,GERMS +,REDS,GERMY +,REED,GESSO +,REEF,GETUP +,REEK,GHATS +,REEL,GHOST +,REFS,GHOUL +,REGS,GIANT +,REIN,GIBES +,RELY,GIDDY +,REND,GIFTS +,RENO,GILDS +,RENT,GILET +,REPO,GILLS +,REPS,GILTS +,REST,GIMME +,RETE,GIMPS +,REVS,GIPSY +,RIAD,GIRDS +,RIAL,GIRLS +,RIBS,GIRLY +,RICE,GIROS +,RICH,GIRTH +,RICK,GISMO +,RIDE,GITES +,RIDS,GIVEN +,RIFE,GIVER +,RIFF,GIVES +,RIFT,GIZMO +,RIGS,GLADE +,RILE,GLADS +,RILL,GLAND +,RIME,GLANS +,RIMS,GLARE +,RIMY,GLASS +,RIND,GLAZE +,RING,GLEAM +,RINK,GLEAN +,RIOT,GLENS +,RIPE,GLIDE +,RIPS,GLINT +,RISE,GLITZ +,RISK,GLOAT +,RITE,GLOBE +,RIVE,GLOBS +,ROAD,GLOOM +,ROAM,GLOOP +,ROAN,GLORY +,ROAR,GLOSS +,ROBE,GLOVE +,ROBS,GLOWS +,ROCK,GLUED +,RODE,GLUES +,RODS,GLUEY +,ROIL,GLUON +,ROLE,GLUTE +,ROLL,GLUTS +,ROMA,GLYPH +,ROMP,GNARL +,ROOF,GNASH +,ROOK,GNATS +,ROOM,GNAWS +,ROOS,GNOME +,ROOT,GOADS +,ROPE,GOALS +,ROPY,GOATS +,RORT,GOBBY +,ROSE,GODLY +,ROSY,GOERS +,ROTA,GOETH +,ROTE,GOFER +,ROTI,GOING +,ROTS,GOLDS +,ROUT,GOLEM +,ROUX,GOLFS +,ROVE,GOLLY +,ROWS,GONAD +,RUBE,GONER +,RUBS,GONGS +,RUBY,GONZO +,RUCK,GOODS +,RUDE,GOODY +,RUED,GOOEY +,RUES,GOOFS +,RUFF,GOOFY +,RUGS,GOONS +,RUIN,GOOSE +,RULE,GORED +,RUMP,GORES +,RUMS,GORGE +,RUNE,GORSE +,RUNG,GOUGE +,RUNS,GOURD +,RUNT,GOUTY +,RUSE,GOWNS +,RUSH,GRABS +,RUSK,GRACE +,RUST,GRADE +,RUTS,GRADS +,RYOT,GRAFT +,SAAG,GRAIL +,SACK,GRAIN +,SACS,GRAMS +,SAFE,GRAND +,SAGA,GRANS +,SAGE,GRANT +,SAGO,GRAPE +,SAGS,GRAPH +,SAID,GRASP +,SAIL,GRASS +,SAKE,GRATE +,SAKI,GRAVE +,SALE,GRAVY +,SALT,GRAYS +,SAME,GRAZE +,SAND,GREAT +,SANE,GREBE +,SANG,GREED +,SANK,GREEK +,SANS,GREEN +,SAPS,GREET +,SARI,GREYS +,SARK,GRIDS +,SASH,GRIEF +,SASS,GRIFT +,SATE,GRILL +,SATI,GRIME +,SAVE,GRIMY +,SAWN,GRIND +,SAWS,GRINS +,SAYS,GRIOT +,SCAB,GRIPE +,SCAM,GRIPS +,SCAN,GRIST +,SCAR,GRITS +,SCAT,GROAN +,SCOT,GROAT +,SCRY,GROIN +,SCUD,GROOM +,SCUM,GROPE +,SCUT,GROSS +,SEAL,GROUP +,SEAM,GROUT +,SEAR,GROVE +,SEAS,GROWL +,SEAT,GROWN +,SECS,GROWS +,SECT,GRUBS +,SEED,GRUEL +,SEEK,GRUFF +,SEEM,GRUMP +,SEEN,GRUNT +,SEEP,GUANO +,SEER,GUARD +,SEES,GUAVA +,SELF,GUESS +,SELL,GUEST +,SEMI,GUIDE +,SEND,GUILD +,SENS,GUILE +,SENT,GUILT +,SEPT,GUISE +,SERA,GULAG +,SERF,GULCH +,SESH,GULES +,SETS,GULFS +,SETT,GULLS +,SEWN,GULLY +,SEWS,GULPS +,SEXY,GUMBO +,SHAD,GUMMY +,SHAG,GUNGE +,SHAH,GUNKY +,SHAM,GUNNY +,SHAW,GUPPY +,SHEA,GURUS +,SHED,GUSHY +,SHES,GUSSY +,SHEW,GUSTO +,SHIM,GUSTS +,SHIN,GUSTY +,SHIP,GUTSY +,SHIV,GUTTY +,SHOD,GUYOT +,SHOE,GYPSY +,SHOO,GYRED +,SHOP,GYROS +,SHOT,GYRUS +,SHOW,HABIT +,SHUN,HACKS +,SHUT,HADST +,SICK,HAIKU +,SICS,HAILS +,SIDE,HAIRS +,SIFT,HAIRY +,SIGH,HAJIS +,SIGN,HAJJI +,SIGS,HAKAS +,SILK,HAKES +,SILL,HALAL +,SILO,HALLO +,SILT,HALLS +,SIMP,HALOS +,SIMS,HALTS +,SINE,HALVA +,SING,HALVE +,SINK,HAMMY +,SINS,HANDS +,SIPS,HANDY +,SIRE,HANGS +,SIRS,HANKY +,SITE,HAPPY +,SITS,HARAM +,SIZE,HARDS +,SKEP,HARDY +,SKEW,HARED +,SKID,HAREM +,SKIM,HARES +,SKIN,HARKS +,SKIP,HARMS +,SKIS,HARPS +,SKIT,HARPY +,SKUA,HARRY +,SKYR,HARSH +,SLAB,HARTS +,SLAG,HASPS +,SLAM,HASTE +,SLAP,HASTY +,SLAT,HATCH +,SLAW,HATED +,SLAY,HATER +,SLED,HATES +,SLEW,HAULM +,SLID,HAULS +,SLIM,HAUNT +,SLIP,HAUTE +,SLIT,HAVEN +,SLOB,HAVES +,SLOE,HAVOC +,SLOG,HAWKS +,SLOP,HAZED +,SLOT,HAZEL +,SLOW,HAZES +,SLUG,HEADS +,SLUM,HEADY +,SLUR,HEALS +,SLUT,HEAPS +,SMOG,HEARD +,SMUG,HEARS +,SMUT,HEART +,SNAG,HEATH +,SNAP,HEATS +,SNIP,HEAVE +,SNIT,HEAVY +,SNOB,HEDGE +,SNOG,HEEDS +,SNOT,HEELS +,SNOW,HEFTS +,SNUB,HEFTY +,SNUG,HEIGH +,SOAK,HEIRS +,SOAP,HEIST +,SOAR,HELIO +,SOBA,HELIX +,SOBS,HELLO +,SOCA,HELMS +,SOCK,HELPS +,SODA,HENCE +,SODS,HENCH +,SOFA,HENGE +,SOFT,HENNA +,SOIL,HENRY +,SOLD,HERBS +,SOLE,HERBY +,SOLO,HERDS +,SOLS,HERON +,SOMA,HERTZ +,SOME,HEWED +,SONG,HEXED +,SONS,HEXES +,SOOK,HICKS +,SOON,HIDES +,SOOT,HIGHS +,SOPS,HIJAB +,SORE,HIJRA +,SORT,HIKED +,SOTS,HIKER +,SOUK,HIKES +,SOUL,HILLS +,SOUP,HILLY +,SOUR,HILTS +,SOUS,HILUM +,SOWN,HINDS +,SOWS,HINGE +,SOYA,HINKY +,SPAM,HINTS +,SPAN,HIPPO +,SPAR,HIPPY +,SPAS,HIRED +,SPAT,HIRES +,SPAY,HITCH +,SPEC,HIVED +,SPED,HIVES +,SPEW,HOARD +,SPIN,HOARY +,SPIT,HOBBY +,SPIV,HOBOS +,SPOD,HOCKS +,SPOT,HOCUS +,SPRY,HOICK +,SPUD,HOIST +,SPUN,HOKEY +,SPUR,HOKUM +,STAB,HOLDS +,STAG,HOLED +,STAR,HOLES +,STAT,HOLLY +,STAY,HOLTS +,STEM,HOMED +,STEP,HOMER +,STEW,HOMES +,STIR,HOMEY +,STOP,HOMIE +,STOW,HOMOS +,STUB,HONED +,STUD,HONES +,STUN,HONEY +,STYE,HONKS +,SUBS,HONOR +,SUCH,HOOCH +,SUCK,HOODS +,SUDS,HOODY +,SUED,HOOEY +,SUES,HOOFS +,SUET,HOOKS +,SUIT,HOOKY +,SULK,HOOPS +,SUMO,HOOTS +,SUMP,HOPED +,SUMS,HOPES +,SUNG,HORDE +,SUNK,HORNS +,SUNS,HORNY +,SUPS,HORSE +,SUQS,HORSY +,SURE,HOSED +,SURF,HOSEL +,SUSS,HOSER +,SWAB,HOSES +,SWAG,HOSTA +,SWAM,HOSTS +,SWAN,HOTEL +,SWAP,HOTLY +,SWAT,HOUND +,SWAY,HOURS +,SWIG,HOUSE +,SWIM,HOVEL +,SWIZ,HOVER +,SWOP,HOWDY +,SWOT,HOWLS +,SWUM,HOYAS +,SYNC,HUBBY +,SYNE,HUFFS +,TABI,HUFFY +,TABS,HULAS +,TACK,HULKS +,TACO,HULLO +,TACT,HULLS +,TAGS,HUMAN +,TAIL,HUMIC +,TAKA,HUMID +,TAKE,HUMOR +,TALC,HUMPH +,TALE,HUMPS +,TALK,HUMPY +,TALL,HUMUS +,TAME,HUNCH +,TAMP,HUNKS +,TANG,HUNKY +,TANK,HUNTS +,TANS,HURLS +,TAPE,HURRY +,TAPS,HURTS +,TAPU,HUSKS +,TARE,HUSKY +,TARN,HUSSY +,TARO,HUTCH +,TARP,HYDRA +,TARS,HYDRO +,TART,HYENA +,TASE,HYGGE +,TASH,HYMEN +,TASK,HYMNS +,TATS,HYOID +,TAUS,HYPED +,TAUT,HYPER +,TAXI,HYPES +,TEAK,HYPHA +,TEAL,HYSON +,TEAM,IAMBS +,TEAR,ICHOR +,TEAS,ICIER +,TEAT,ICILY +,TECH,ICING +,TEED,ICONS +,TEEM,IDEAL +,TEEN,IDEAS +,TEES,IDENT +,TEFF,IDIOM +,TELE,IDIOT +,TELL,IDLED +,TEMP,IDLER +,TEND,IDLES +,TENS,IDOLS +,TENT,IDYLL +,TERF,IFTAR +,TERM,IGLOO +,TERN,IKONS +,TEST,ILEUM +,TEXT,ILEUS +,THAN,ILIAC +,THAT,ILIUM +,THAW,IMAGE +,THEE,IMAMS +,THEM,IMBED +,THEN,IMBUE +,THEW,IMPEL +,THEY,IMPLY +,THIN,INANE +,THIS,INAPT +,THOU,INBOX +,THRU,INCEL +,THUD,INCUR +,THUG,INCUS +,THUS,INDEX +,TICK,INDIE +,TICS,INEPT +,TIDE,INERT +,TIDY,INFER +,TIED,INFIX +,TIER,INFRA +,TIES,INGOT +,TIFF,INION +,TIFO,INKED +,TIKE,INLAY +,TIKI,INLET +,TILE,INNER +,TILL,INPUT +,TILT,INSET +,TIME,INSPO +,TINE,INTEL +,TING,INTER +,TINS,INTRO +,TINT,INURE +,TINY,IONIC +,TIPI,IOTAS +,TIPS,IRATE +,TIRE,IRKED +,TITS,IRONS +,TIZZ,IRONY +,TOAD,ISLES +,TOBY,ISLET +,TOCK,ISSUE +,TOED,ITCHY +,TOES,ITEMS +,TOFF,IVIED +,TOFU,IVIES +,TOGA,IVORY +,TOGS,IXNAY +,TOIL,JACKS +,TOKE,JADED +,TOLD,JADES +,TOLL,JAFFA +,TOMB,JAGGY +,TOME,JAILS +,TOMS,JAMBS +,TONE,JAMMY +,TONG,JANES +,TONS,JANKY +,TONY,JAPAN +,TOOK,JAPES +,TOOL,JAUNT +,TOOT,JAZZY +,TOPS,JEANS +,TORE,JEEPS +,TORN,JEERS +,TORR,JELLO +,TORS,JELLS +,TORT,JELLY +,TORY,JENNY +,TOSA,JERKS +,TOSH,JERKY +,TOSS,JERRY +,TOTE,JESTS +,TOTS,JETTY +,TOUR,JEWEL +,TOUT,JIBED +,TOWN,JIBES +,TOWS,JIFFS +,TOYS,JIFFY +,TRAM,JIGGY +,TRAP,JILTS +,TRAY,JIMMY +,TREE,JINGO +,TREK,JINKS +,TRIG,JINNS +,TRIM,JIVED +,TRIO,JIVES +,TRIP,JOCKS +,TROD,JOEYS +,TROT,JOHNS +,TRUE,JOINS +,TRUG,JOINT +,TUBA,JOIST +,TUBE,JOKED +,TUBS,JOKER +,TUCK,JOKES +,TUFF,JOKEY +,TUFT,JOLLY +,TUGS,JOLTS +,TUMS,JONES +,TUNA,JOULE +,TUNE,JOUST +,TUNS,JOWLS +,TURD,JUDAS +,TURF,JUDGE +,TURN,JUDGY +,TUSH,JUICE +,TUSK,JUICY +,TUTS,JUKED +,TUTU,JUKES +,TWAT,JULEP +,TWEE,JUMBO +,TWIG,JUMPS +,TWIN,JUMPY +,TWIT,JUNKS +,TWOS,JUNKY +,TYKE,JUNTA +,TYPE,JUNTO +,TYPO,JUROR +,TYRE,JUVIE +,TYRO,KABOB +,UDON,KANJI +,UFOS,KAPOK +,UGLY,KAPPA +,UKES,KAPUT +,ULNA,KARAT +,UMBO,KARMA +,UMPS,KARST +,UNDO,KARTS +,UNIS,KASHA +,UNIT,KATAS +,UNTO,KAYAK +,UPDO,KAZOO +,UPON,KEBAB +,UREA,KECKS +,URGE,KEELS +,URIC,KEENS +,URNS,KEEPS +,USED,KEFIR +,USER,KENDO +,USES,KERBS +,UTES,KETCH +,UVEA,KEYED +,VACS,KHAKI +,VAIL,KICKS +,VAIN,KICKY +,VALE,KIDDO +,VAMP,KIDDY +,VANE,KILLS +,VANS,KILNS +,VAPE,KILOS +,VARY,KILTS +,VASE,KINDA +,VAST,KINDS +,VATS,KINGS +,VEAL,KINKS +,VEEP,KINKY +,VEER,KIOSK +,VEES,KIPPA +,VEIL,KIRKS +,VEIN,KITES +,VELD,KITTY +,VEND,KIVAS +,VENT,KIWIS +,VERB,KLUTZ +,VERT,KNACK +,VERY,KNAVE +,VEST,KNEAD +,VETO,KNEED +,VETS,KNEEL +,VIAL,KNEES +,VIBE,KNELL +,VICE,KNELT +,VIDE,KNIFE +,VIED,KNISH +,VIES,KNITS +,VIEW,KNOBS +,VILE,KNOCK +,VINE,KNOLL +,VINO,KNOTS +,VIOL,KNOWN +,VISA,KNOWS +,VISE,KOALA +,VIVA,KOFTA +,VIVO,KOMBU +,VLOG,KOOKS +,VOID,KOOKY +,VOLE,KORAS +,VOLT,KORMA +,VOTE,KRAUT +,VOWS,KRILL +,WACK,KUDOS +,WADE,KUGEL +,WADI,LABEL +,WADS,LABIA +,WAFT,LABOR +,WAGE,LACED +,WAGS,LACES +,WAIF,LACKS +,WAIL,LADEN +,WAIN,LADLE +,WAIT,LAGER +,WAKE,LAGGY +,WALK,LAHAR +,WALL,LAIRD +,WAND,LAIRS +,WANE,LAITY +,WANS,LAKES +,WANT,LAKSA +,WARD,LAMAS +,WARE,LAMBS +,WARM,LAMER +,WARN,LAMPS +,WARP,LANAI +,WARS,LANCE +,WART,LANDS +,WARY,LANES +,WASH,LANKY +,WASP,LAPEL +,WATT,LAPSE +,WAVE,LARCH +,WAVY,LARDS +,WAXY,LARGE +,WAYS,LARKS +,WEAK,LARVA +,WEAL,LASER +,WEAN,LASSI +,WEAR,LASSO +,WEBS,LASTS +,WEDS,LATCH +,WEED,LATER +,WEEK,LATEX +,WEEP,LATHE +,WEES,LATHS +,WEFT,LATKE +,WEIR,LATTE +,WELD,LAUDS +,WELL,LAUGH +,WELP,LAVER +,WELT,LAWNS +,WENT,LAYER +,WEPT,LAYUP +,WERE,LAZED +,WEST,LAZES +,WETA,LEADS +,WETS,LEAFS +,WHAM,LEAFY +,WHAT,LEAKS +,WHEE,LEAKY +,WHEN,LEANS +,WHET,LEANT +,WHEW,LEAPS +,WHEY,LEAPT +,WHIG,LEARN +,WHIM,LEASE +,WHIN,LEASH +,WHIP,LEAST +,WHIR,LEAVE +,WHIT,LEDES +,WHIZ,LEDGE +,WHOA,LEECH +,WHOM,LEEKS +,WHOP,LEERS +,WHUP,LEERY +,WICK,LEFTS +,WIDE,LEFTY +,WIFE,LEGAL +,WIGS,LEGGY +,WIKI,LEGIT +,WILD,LEMMA +,WILL,LEMON +,WILT,LEMUR +,WILY,LENDS +,WIMP,LENIS +,WIND,LEPER +,WINE,LETCH +,WING,LETUP +,WINK,LEVEE +,WINO,LEVEL +,WINS,LEVER +,WIPE,LEWIS +,WIRE,LEXIS +,WIRY,LIANA +,WISE,LIARS +,WISH,LIBEL +,WISP,LIBRA +,WITH,LICIT +,WITS,LICKS +,WOAD,LIDOS +,WOES,LIEGE +,WOKE,LIENS +,WOKS,LIFER +,WOLD,LIFTS +,WOLF,LIGHT +,WOMB,LIKED +,WONK,LIKEN +,WONT,LIKES +,WOOD,LILAC +,WOOF,LIMBO +,WOOL,LIMBS +,WOOS,LIMED +,WOOT,LIMES +,WOPS,LIMEY +,WORD,LIMIT +,WORE,LIMNS +,WORK,LIMOS +,WORM,LIMPS +,WORN,LINED +,WORT,LINEN +,WOVE,LINER +,WOWS,LINES +,WRAP,LINGO +,WREN,LINKS +,WRIT,LINUX +,WUSS,LIONS +,YACK,LIPID +,YAKS,LIPPY +,YAMS,LISLE +,YANG,LISPS +,YANK,LISTS +,YAPP,LITER +,YAPS,LITHE +,YARD,LITHO +,YARN,LITRE +,YAWN,LIVED +,YAWS,LIVEN +,YEAH,LIVER +,YEAR,LIVES +,YEAS,LIVID +,YECH,LLAMA +,YEET,LOADS +,YELL,LOAFS +,YELP,LOANS +,YEOW,LOATH +,YETI,LOBAR +,YEWS,LOBBY +,YOBS,LOBES +,YOGA,LOCAL +,YOGI,LOCHS +,YOKE,LOCKS +,YOLK,LOCOS +,YOMP,LOCUM +,YOOF,LOCUS +,YORE,LODES +,YOUR,LODGE +,YOUS,LOESS +,YOWL,LOFTS +,YUAN,LOFTY +,YUCK,LOGES +,YULE,LOGIC +,YURT,LOGIN +,YUZU,LOGON +,ZAGS,LOGOS +,ZANY,LOINS +,ZAPS,LOLLS +,ZEAL,LOLLY +,ZEBU,LONER +,ZEDS,LONGS +,ZEES,LOOKS +,ZERO,LOOMS +,ZEST,LOONS +,ZETA,LOONY +,ZHOS,LOOPS +,ZIGS,LOOPY +,ZINC,LOOSE +,ZINE,LOOTS +,ZING,LOPED +,ZIPS,LOPES +,ZITI,LORDS +,ZITS,LORDY +,ZONE,LORIS +,ZONK,LORRY +,ZOOM,LOSER +,ZOOS,LOSES +,ZOUK,LOSSY +,,LOTTO +,,LOTUS +,,LOUGH +,,LOUSE +,,LOUSY +,,LOUTS +,,LOVED +,,LOVER +,,LOVES +,,LOVEY +,,LOWER +,,LOWLY +,,LOYAL +,,LUBED +,,LUBES +,,LUCID +,,LUCKS +,,LUCKY +,,LUCRE +,,LUDIC +,,LUGES +,,LULLS +,,LULUS +,,LUMEN +,,LUMPS +,,LUMPY +,,LUNAR +,,LUNCH +,,LUNGE +,,LUNGS +,,LUPIN +,,LUPUS +,,LURCH +,,LURED +,,LURES +,,LUREX +,,LURGY +,,LURID +,,LURKS +,,LURVE +,,LUSTS +,,LUSTY +,,LUTES +,,LUVVY +,,LYCEE +,,LYCRA +,,LYING +,,LYMPH +,,LYNCH +,,LYRES +,,LYRIC +,,LYSIN +,,LYSIS +,,MACAW +,,MACES +,,MACHE +,,MACHO +,,MACKS +,,MACRO +,,MADAM +,,MADLY +,,MAFIA +,,MAFIC +,,MAGIC +,,MAGMA +,,MAIDS +,,MAILS +,,MAIMS +,,MAINS +,,MAIZE +,,MAJOR +,,MAKER +,,MAKES +,,MALES +,,MALLS +,,MALTS +,,MALTY +,,MAMAS +,,MAMBA +,,MAMBO +,,MAMMA +,,MAMMY +,,MANES +,,MANGA +,,MANGE +,,MANGO +,,MANGY +,,MANIA +,,MANIC +,,MANKY +,,MANLY +,,MANNA +,,MANOR +,,MANSE +,,MANTA +,,MAPLE +,,MARCH +,,MARDY +,,MARES +,,MARIA +,,MARKS +,,MARRY +,,MARSH +,,MARTS +,,MASKS +,,MASON +,,MASTS +,,MATCH +,,MATED +,,MATER +,,MATES +,,MATEY +,,MATHS +,,MATTE +,,MATZO +,,MAULS +,,MAUVE +,,MAVEN +,,MAXED +,,MAXES +,,MAXIM +,,MAXIS +,,MAYBE +,,MAYOR +,,MAYOS +,,MAZES +,,MEALS +,,MEALY +,,MEANS +,,MEANT +,,MEANY +,,MEATS +,,MEATY +,,MEDAL +,,MEDIA +,,MEDIC +,,MEETS +,,MELBA +,,MELDS +,,MELEE +,,MELON +,,MELTS +,,MEMES +,,MEMOS +,,MENDS +,,MENUS +,,MEOWS +,,MERCH +,,MERCS +,,MERCY +,,MERGE +,,MERIT +,,MERRY +,,MESAS +,,MESON +,,MESSY +,,METAL +,,METED +,,METER +,,METES +,,METRE +,,METRO +,,MEWED +,,MEWLS +,,MEZES +,,MEZZE +,,MEZZO +,,MIAOW +,,MICRO +,,MIDDY +,,MIDGE +,,MIDIS +,,MIDST +,,MIENS +,,MIGHT +,,MIKED +,,MIKES +,,MILES +,,MILFS +,,MILKS +,,MILKY +,,MILLS +,,MIMED +,,MIMES +,,MIMIC +,,MINCE +,,MINDS +,,MINED +,,MINER +,,MINES +,,MINGE +,,MINGY +,,MINIM +,,MINIS +,,MINKE +,,MINKS +,,MINOR +,,MINTS +,,MINUS +,,MIRED +,,MIRES +,,MIRIN +,,MIRTH +,,MISER +,,MISSY +,,MISTS +,,MISTY +,,MITER +,,MITES +,,MITRE +,,MITTS +,,MIXED +,,MIXER +,,MIXES +,,MOANS +,,MOATS +,,MOCHA +,,MOCKS +,,MODAL +,,MODEL +,,MODEM +,,MODES +,,MODUS +,,MOGGY +,,MOGUL +,,MOHEL +,,MOIRE +,,MOIST +,,MOLAL +,,MOLAR +,,MOLDS +,,MOLDY +,,MOLES +,,MOLLS +,,MOLTS +,,MOMMA +,,MOMMY +,,MONDE +,,MONEY +,,MONKS +,,MONTE +,,MONTH +,,MOOBS +,,MOOCH +,,MOODS +,,MOODY +,,MOOED +,,MOOKS +,,MOONS +,,MOONY +,,MOORS +,,MOOSE +,,MOOTS +,,MOPED +,,MOPES +,,MORAL +,,MOREL +,,MORES +,,MORON +,,MORPH +,,MORSE +,,MOSEY +,,MOSSY +,,MOTEL +,,MOTES +,,MOTHS +,,MOTIF +,,MOTOR +,,MOTTE +,,MOTTO +,,MOULD +,,MOULT +,,MOUND +,,MOUNT +,,MOURN +,,MOUSE +,,MOUSY +,,MOUTH +,,MOVED +,,MOVER +,,MOVES +,,MOVIE +,,MOWED +,,MOWER +,,MOXIE +,,MUCKS +,,MUCKY +,,MUCUS +,,MUDDY +,,MUFFS +,,MUFTI +,,MUGGY +,,MULCH +,,MULCT +,,MULES +,,MULEY +,,MULGA +,,MULLS +,,MUMMY +,,MUMPS +,,MUMSY +,,MUNCH +,,MUNIS +,,MUONS +,,MURAL +,,MURKY +,,MUSED +,,MUSES +,,MUSHY +,,MUSIC +,,MUSKY +,,MUSOS +,,MUSTS +,,MUSTY +,,MUTED +,,MUTES +,,MUTTS +,,MUZAK +,,MUZZY +,,MYLAR +,,MYNAH +,,MYRRH +,,MYTHS +,,NAANS +,,NABOB +,,NACHO +,,NACRE +,,NADIR +,,NAIAD +,,NAIFS +,,NAILS +,,NAIVE +,,NAKED +,,NAMED +,,NAMES +,,NANAS +,,NANNY +,,NAPES +,,NAPPY +,,NARCS +,,NARIS +,,NARKS +,,NARKY +,,NASAL +,,NASTY +,,NATAL +,,NATCH +,,NATES +,,NATTY +,,NAVAL +,,NAVEL +,,NAVES +,,NAVVY +,,NAZIS +,,NDUJA +,,NEARS +,,NEATH +,,NECKS +,,NEEDS +,,NEEDY +,,NEEPS +,,NEIGH +,,NELLY +,,NERDS +,,NERDY +,,NERVE +,,NERVY +,,NESTS +,,NEVER +,,NEWER +,,NEWLY +,,NEWSY +,,NEWTS +,,NEXUS +,,NICAD +,,NICER +,,NICHE +,,NICKS +,,NIECE +,,NIFFS +,,NIFTY +,,NIGHT +,,NINES +,,NINJA +,,NINNY +,,NINTH +,,NIPPY +,,NIQAB +,,NITES +,,NIXED +,,NIXES +,,NOBLE +,,NOBLY +,,NOCKS +,,NODAL +,,NODES +,,NOHOW +,,NOIRS +,,NOISE +,,NOISY +,,NOMAD +,,NONCE +,,NONES +,,NONIS +,,NOOBS +,,NOOKS +,,NOOKY +,,NOOSE +,,NOPAL +,,NORMS +,,NORTH +,,NOSED +,,NOSES +,,NOSEY +,,NOTCH +,,NOTED +,,NOTES +,,NOUNS +,,NOVAE +,,NOVAS +,,NOVEL +,,NUBBY +,,NUDES +,,NUDGE +,,NUKED +,,NUKES +,,NUMBS +,,NURSE +,,NUTTY +,,NYLON +,,NYMPH +,,OAKEN +,,OAKUM +,,OASES +,,OASIS +,,OATER +,,OATHS +,,OBESE +,,OBEYS +,,OBITS +,,OBOES +,,OCCUR +,,OCEAN +,,OCHER +,,OCHRE +,,OCKER +,,OCTAD +,,OCTAL +,,OCTAN +,,OCTET +,,OCULI +,,ODDER +,,ODDLY +,,ODIUM +,,ODORS +,,ODOUR +,,OFFAL +,,OFFED +,,OFFER +,,OFFIE +,,OFTEN +,,OGIVE +,,OGLED +,,OGLES +,,OGRES +,,OILED +,,OILER +,,OINKS +,,OKAPI +,,OKAYS +,,OLDEN +,,OLDER +,,OLDIE +,,OLIVE +,,OLOGY +,,OMBRE +,,OMEGA +,,OMENS +,,OMITS +,,ONION +,,ONSET +,,OOMPH +,,OOZED +,,OOZES +,,OPALS +,,OPENS +,,OPERA +,,OPINE +,,OPIUM +,,OPTED +,,OPTIC +,,ORACH +,,ORACY +,,ORALS +,,ORBIT +,,ORCAS +,,ORDER +,,ORGAN +,,OTAKU +,,OTHER +,,OTTER +,,OUGHT +,,OUNCE +,,OUSTS +,,OUTDO +,,OUTED +,,OUTER +,,OUTGO +,,OUTRE +,,OUTRO +,,OUTTA +,,OUZOS +,,OVALS +,,OVARY +,,OVATE +,,OVENS +,,OVERS +,,OVERT +,,OVINE +,,OVOID +,,OVULE +,,OWING +,,OWLET +,,OWNED +,,OWNER +,,OXBOW +,,OXIDE +,,OXLIP +,,OXTER +,,OZONE +,,PACED +,,PACES +,,PACEY +,,PACKS +,,PACTS +,,PADDY +,,PADRE +,,PAEAN +,,PAGAN +,,PAGED +,,PAGER +,,PAGES +,,PAILS +,,PAINS +,,PAINT +,,PAIRS +,,PALED +,,PALER +,,PALES +,,PALLS +,,PALLY +,,PALMS +,,PALMY +,,PALSY +,,PANDA +,,PANEL +,,PANES +,,PANGS +,,PANIC +,,PANKO +,,PANSY +,,PANTS +,,PANTY +,,PAPAL +,,PAPAS +,,PAPAW +,,PAPER +,,PAPPY +,,PARCH +,,PARED +,,PARES +,,PARKA +,,PARKS +,,PARKY +,,PARPS +,,PARRY +,,PARSE +,,PARTS +,,PARTY +,,PASEO +,,PASHA +,,PASSE +,,PASTA +,,PASTE +,,PASTS +,,PASTY +,,PATCH +,,PATER +,,PATHS +,,PATIO +,,PATSY +,,PATTY +,,PAUSE +,,PAVED +,,PAVER +,,PAVES +,,PAWED +,,PAWKY +,,PAWNS +,,PAYEE +,,PAYER +,,PEACE +,,PEACH +,,PEAKS +,,PEAKY +,,PEALS +,,PEARL +,,PEARS +,,PEATY +,,PECAN +,,PECKS +,,PEDAL +,,PEDIS +,,PEEKS +,,PEELS +,,PEEPS +,,PEERS +,,PEEVE +,,PEKOE +,,PELTS +,,PENAL +,,PENCE +,,PENIS +,,PENNE +,,PENNY +,,PEONS +,,PEONY +,,PEPPY +,,PERCH +,,PERIL +,,PERKS +,,PERKY +,,PERMS +,,PERPS +,,PERRY +,,PERVS +,,PERVY +,,PESKY +,,PESOS +,,PESTO +,,PESTS +,,PETAL +,,PETER +,,PETIT +,,PETTY +,,PHAGE +,,PHASE +,,PHIAL +,,PHLOX +,,PHONE +,,PHONY +,,PHOTO +,,PHYLA +,,PIANO +,,PICKS +,,PICKY +,,PIECE +,,PIERS +,,PIETY +,,PIGGY +,,PIGMY +,,PIKER +,,PIKES +,,PILAF +,,PILAU +,,PILED +,,PILES +,,PILLS +,,PILOT +,,PIMPS +,,PINCH +,,PINED +,,PINES +,,PINEY +,,PINGS +,,PINKO +,,PINKS +,,PINKY +,,PINNA +,,PINNY +,,PINON +,,PINOT +,,PINTA +,,PINTO +,,PINTS +,,PIOUS +,,PIPAS +,,PIPED +,,PIPER +,,PIPES +,,PIQUE +,,PISCO +,,PISSY +,,PISTE +,,PITAS +,,PITCH +,,PITHS +,,PITHY +,,PITON +,,PITTA +,,PIVOT +,,PIXEL +,,PIXIE +,,PIZZA +,,PLACE +,,PLAID +,,PLAIN +,,PLAIT +,,PLANE +,,PLANK +,,PLANS +,,PLANT +,,PLASH +,,PLASM +,,PLATE +,,PLATS +,,PLAYS +,,PLAZA +,,PLEAD +,,PLEAS +,,PLEAT +,,PLEBS +,,PLICA +,,PLIED +,,PLIER +,,PLIES +,,PLINK +,,PLODS +,,PLONK +,,PLOPS +,,PLOTS +,,PLOWS +,,PLOYS +,,PLUCK +,,PLUGS +,,PLUMB +,,PLUME +,,PLUMP +,,PLUMS +,,PLUMY +,,PLUNK +,,PLUSH +,,POACH +,,POCKS +,,PODGY +,,PODIA +,,POEMS +,,POESY +,,POETS +,,POINT +,,POISE +,,POKED +,,POKER +,,POKES +,,POKEY +,,POLAR +,,POLED +,,POLES +,,POLIO +,,POLIS +,,POLKA +,,POLLS +,,POLYP +,,POLYS +,,POMMY +,,PONCE +,,PONCY +,,PONDS +,,PONGS +,,PONZU +,,POOCH +,,POOED +,,POOHS +,,POOLS +,,POOPS +,,POPES +,,POPPY +,,PORCH +,,PORED +,,PORES +,,PORGY +,,PORKS +,,PORKY +,,PORNO +,,PORNY +,,PORTA +,,PORTS +,,POSED +,,POSER +,,POSES +,,POSEY +,,POSIT +,,POSSE +,,POSTS +,,POTTY +,,POUCH +,,POUFS +,,POULT +,,POUND +,,POURS +,,POUTS +,,POUTY +,,POWER +,,PRAMS +,,PRANG +,,PRANK +,,PRATE +,,PRATS +,,PRAWN +,,PRAYS +,,PREEN +,,PREON +,,PREPS +,,PRESS +,,PREYS +,,PRICE +,,PRICK +,,PRICY +,,PRIDE +,,PRIED +,,PRIES +,,PRIGS +,,PRIME +,,PRIMO +,,PRIMP +,,PRINT +,,PRION +,,PRIOR +,,PRISE +,,PRISM +,,PRIVY +,,PRIZE +,,PROBE +,,PROBS +,,PRODS +,,PROFS +,,PROLE +,,PROMO +,,PROMS +,,PRONE +,,PRONG +,,PRONK +,,PROOF +,,PROPS +,,PROSE +,,PROSY +,,PROTO +,,PROUD +,,PROVE +,,PROWL +,,PROWS +,,PROXY +,,PRUDE +,,PRUNE +,,PSALM +,,PSEUD +,,PSOAS +,,PSYCH +,,PUBES +,,PUBIC +,,PUBIS +,,PUCKS +,,PUDGY +,,PUFFA +,,PUFFS +,,PUFFY +,,PUKED +,,PUKES +,,PUKKA +,,PULLS +,,PULPS +,,PULPY +,,PULSE +,,PUMAS +,,PUMPS +,,PUNCH +,,PUNKS +,,PUNKY +,,PUNNY +,,PUNTS +,,PUNTY +,,PUPAE +,,PUPIL +,,PUPPY +,,PUREE +,,PURER +,,PURGE +,,PURLS +,,PURRS +,,PURSE +,,PUSHY +,,PUSSY +,,PUTTS +,,PUTTY +,,PWNED +,,PYGMY +,,PYLON +,,PYRES +,,PZAZZ +,,QUACK +,,QUADS +,,QUAFF +,,QUAIL +,,QUAKE +,,QUALM +,,QUANT +,,QUARK +,,QUART +,,QUASH +,,QUASI +,,QUAYS +,,QUBIT +,,QUEEN +,,QUEER +,,QUELL +,,QUERN +,,QUERY +,,QUESO +,,QUEST +,,QUEUE +,,QUICK +,,QUIET +,,QUIFF +,,QUILL +,,QUILT +,,QUINT +,,QUIPS +,,QUIRE +,,QUIRK +,,QUITE +,,QUITS +,,QUOLL +,,QUOTA +,,QUOTE +,,QUOTH +,,RABBI +,,RABID +,,RACED +,,RACER +,,RACES +,,RACKS +,,RADAR +,,RADII +,,RADIO +,,RADIX +,,RADON +,,RAFTS +,,RAGAS +,,RAGED +,,RAGES +,,RAGGA +,,RAGUS +,,RAIDS +,,RAILS +,,RAINS +,,RAINY +,,RAISE +,,RAITA +,,RAKED +,,RAKES +,,RALES +,,RALLY +,,RALPH +,,RAMEN +,,RAMPS +,,RAMUS +,,RANCH +,,RANDO +,,RANDY +,,RANGE +,,RANGY +,,RANKS +,,RANTS +,,RAPED +,,RAPES +,,RAPHE +,,RAPID +,,RARER +,,RASPS +,,RASPY +,,RASTA +,,RATED +,,RATES +,,RATIO +,,RATTY +,,RAVED +,,RAVEL +,,RAVEN +,,RAVER +,,RAVES +,,RAYED +,,RAYON +,,RAZED +,,RAZES +,,RAZOR +,,REACH +,,REACT +,,READS +,,READY +,,REALM +,,REALS +,,REAMS +,,REAPS +,,REARM +,,REARS +,,REBAR +,,REBEC +,,REBEL +,,REBID +,,REBUS +,,REBUT +,,RECAP +,,RECCE +,,RECON +,,RECUR +,,RECUT +,,REDID +,,REDOS +,,REDOX +,,REDUX +,,REEDS +,,REEDY +,,REEFS +,,REEKS +,,REELS +,,REEVE +,,REFER +,,REFIT +,,REGAL +,,REHAB +,,REIFY +,,REIGN +,,REIKI +,,REINS +,,REJIG +,,REKEY +,,RELAX +,,RELAY +,,RELIC +,,RELIT +,,RELLO +,,REMIT +,,REMIX +,,RENAL +,,RENDS +,,RENEW +,,RENTS +,,REPAY +,,REPEL +,,REPLY +,,REPOS +,,REPOT +,,RERAN +,,RERUN +,,RESET +,,RESIN +,,RESIT +,,RESTS +,,RETCH +,,RETIE +,,RETRO +,,RETRY +,,REUSE +,,REVEL +,,REVUE +,,REXES +,,RHEUM +,,RHINO +,,RHOMB +,,RHUMB +,,RHYME +,,RIADS +,,RIALS +,,RICED +,,RICER +,,RICIN +,,RICKS +,,RIDER +,,RIDES +,,RIDGE +,,RIFFS +,,RIFLE +,,RIFTS +,,RIGHT +,,RIGID +,,RIGOR +,,RILED +,,RILES +,,RIMES +,,RINDS +,,RINGS +,,RINKS +,,RINSE +,,RIOJA +,,RIOTS +,,RIPEN +,,RIPER +,,RISEN +,,RISER +,,RISES +,,RISKS +,,RISKY +,,RITES +,,RITZY +,,RIVAL +,,RIVEN +,,RIVER +,,RIVET +,,RIYAL +,,ROACH +,,ROADS +,,ROAMS +,,ROANS +,,ROARS +,,ROAST +,,ROBED +,,ROBES +,,ROBIN +,,ROBOT +,,ROCKS +,,ROCKY +,,RODEO +,,ROGER +,,ROGUE +,,ROIDS +,,ROILS +,,ROLES +,,ROLLS +,,ROMAN +,,ROMEO +,,ROMPS +,,RONDO +,,ROOFS +,,ROOKS +,,ROOMS +,,ROOMY +,,ROOST +,,ROOTS +,,ROPED +,,ROPES +,,ROPEY +,,RORTS +,,ROSES +,,ROSIN +,,ROSTI +,,ROTAS +,,ROTIS +,,ROTOR +,,ROUGE +,,ROUGH +,,ROUND +,,ROUSE +,,ROUST +,,ROUTE +,,ROUTS +,,ROVED +,,ROVER +,,ROVES +,,ROWAN +,,ROWDY +,,ROWED +,,ROWER +,,ROYAL +,,RUBES +,,RUBLE +,,RUCHE +,,RUCKS +,,RUDDY +,,RUDER +,,RUFFS +,,RUGBY +,,RUINS +,,RULED +,,RULER +,,RULES +,,RUMBA +,,RUMMY +,,RUMOR +,,RUMPS +,,RUNES +,,RUNGS +,,RUNNY +,,RUNTS +,,RUPEE +,,RURAL +,,RUSES +,,RUSKS +,,RUSTS +,,RUSTY +,,RYOTS +,,SABER +,,SABLE +,,SABRE +,,SACKS +,,SADDO +,,SADHU +,,SADLY +,,SAFER +,,SAFES +,,SAGAS +,,SAGES +,,SAGGY +,,SAILS +,,SAINT +,,SAKES +,,SAKIS +,,SALAD +,,SALES +,,SALLY +,,SALON +,,SALSA +,,SALTS +,,SALTY +,,SALUT +,,SALVE +,,SALVO +,,SAMBA +,,SAMBO +,,SAMEY +,,SANDS +,,SANDY +,,SANER +,,SAPPY +,,SAREE +,,SARGE +,,SARIN +,,SARIS +,,SARKY +,,SASSY +,,SATAY +,,SATED +,,SATIN +,,SATYR +,,SAUCE +,,SAUCY +,,SAUNA +,,SAUTE +,,SAVED +,,SAVER +,,SAVES +,,SAVOR +,,SAVVY +,,SAWED +,,SAXES +,,SAYED +,,SCABS +,,SCADS +,,SCALD +,,SCALE +,,SCALP +,,SCALY +,,SCAMP +,,SCAMS +,,SCANS +,,SCANT +,,SCAPE +,,SCARE +,,SCARF +,,SCARP +,,SCARS +,,SCART +,,SCARY +,,SCATS +,,SCENE +,,SCENT +,,SCHMO +,,SCHWA +,,SCION +,,SCOFF +,,SCOLD +,,SCONE +,,SCOOP +,,SCOOT +,,SCOPE +,,SCORE +,,SCORN +,,SCOTS +,,SCOUR +,,SCOUT +,,SCOWL +,,SCRAM +,,SCRAP +,,SCREE +,,SCREW +,,SCRIM +,,SCRIP +,,SCROD +,,SCRUB +,,SCRUM +,,SCUBA +,,SCUDS +,,SCUFF +,,SCULL +,,SCUMS +,,SCURF +,,SEALS +,,SEAMS +,,SEAMY +,,SEARS +,,SEATS +,,SEBUM +,,SECTS +,,SEDAN +,,SEDER +,,SEDGE +,,SEDUM +,,SEEDS +,,SEEDY +,,SEEKS +,,SEEMS +,,SEEPS +,,SEERS +,,SEGUE +,,SEINE +,,SEIZE +,,SELLS +,,SEMIS +,,SENDS +,,SENSE +,,SEPAL +,,SEPIA +,,SEPOY +,,SEPTA +,,SEPTS +,,SERFS +,,SERGE +,,SERIF +,,SERUM +,,SERVE +,,SERVO +,,SETTS +,,SETUP +,,SEVEN +,,SEVER +,,SEWED +,,SEWER +,,SEXED +,,SEXES +,,SHACK +,,SHADE +,,SHADY +,,SHAFT +,,SHAGS +,,SHAHS +,,SHAKE +,,SHAKY +,,SHALE +,,SHALL +,,SHALT +,,SHAME +,,SHAMS +,,SHANK +,,SHAPE +,,SHARD +,,SHARE +,,SHARK +,,SHARP +,,SHAVE +,,SHAWL +,,SHAWM +,,SHEAF +,,SHEAR +,,SHEDS +,,SHEEN +,,SHEEP +,,SHEER +,,SHEET +,,SHEIK +,,SHELF +,,SHELL +,,SHEWS +,,SHIED +,,SHIES +,,SHIFT +,,SHILL +,,SHIMS +,,SHINE +,,SHINS +,,SHINY +,,SHIPS +,,SHIRE +,,SHIRK +,,SHIRR +,,SHIRT +,,SHISH +,,SHISO +,,SHITE +,,SHITS +,,SHLEP +,,SHOAL +,,SHOCK +,,SHOES +,,SHOJI +,,SHOJO +,,SHONE +,,SHOOK +,,SHOOS +,,SHOOT +,,SHOPS +,,SHORE +,,SHORN +,,SHORT +,,SHOTS +,,SHOUT +,,SHOVE +,,SHOWN +,,SHOWS +,,SHOWY +,,SHRED +,,SHREW +,,SHRUB +,,SHRUG +,,SHTUM +,,SHUCK +,,SHUNS +,,SHUNT +,,SHUSH +,,SHUTS +,,SHYLY +,,SIBYL +,,SICKO +,,SICKS +,,SIDED +,,SIDES +,,SIDLE +,,SIEGE +,,SIEVE +,,SIGHS +,,SIGHT +,,SIGMA +,,SIGNS +,,SILKS +,,SILKY +,,SILLS +,,SILLY +,,SILOS +,,SILTS +,,SIMPS +,,SINCE +,,SINES +,,SINEW +,,SINGE +,,SINGS +,,SINKS +,,SINUS +,,SIRED +,,SIREN +,,SIRES +,,SIRUP +,,SISAL +,,SISSY +,,SITAR +,,SITED +,,SITES +,,SIXER +,,SIXES +,,SIXTH +,,SIXTY +,,SIZED +,,SIZES +,,SKANK +,,SKATE +,,SKEIN +,,SKEWS +,,SKIDS +,,SKIED +,,SKIER +,,SKIES +,,SKIFF +,,SKILL +,,SKIMP +,,SKIMS +,,SKINK +,,SKINS +,,SKINT +,,SKIPS +,,SKIRL +,,SKIRT +,,SKITS +,,SKIVE +,,SKORT +,,SKUAS +,,SKULK +,,SKULL +,,SKUNK +,,SLABS +,,SLACK +,,SLAGS +,,SLAIN +,,SLAKE +,,SLAMS +,,SLANG +,,SLANT +,,SLAPS +,,SLASH +,,SLATE +,,SLATS +,,SLAVE +,,SLAYS +,,SLEBS +,,SLEDS +,,SLEEK +,,SLEEP +,,SLEET +,,SLEPT +,,SLEWS +,,SLICE +,,SLICK +,,SLIDE +,,SLIME +,,SLIMS +,,SLIMY +,,SLING +,,SLINK +,,SLIPS +,,SLITS +,,SLOBS +,,SLOES +,,SLOGS +,,SLOOP +,,SLOPE +,,SLOPS +,,SLOSH +,,SLOTH +,,SLOTS +,,SLOWS +,,SLUGS +,,SLUMP +,,SLUMS +,,SLUNG +,,SLUNK +,,SLURP +,,SLURS +,,SLUSH +,,SLUTS +,,SLYLY +,,SMACK +,,SMALL +,,SMARM +,,SMART +,,SMASH +,,SMEAR +,,SMELL +,,SMELT +,,SMILE +,,SMIRK +,,SMITE +,,SMITH +,,SMOCK +,,SMOGS +,,SMOKE +,,SMOKY +,,SMORE +,,SMOTE +,,SMUTS +,,SNACK +,,SNAFU +,,SNAGS +,,SNAIL +,,SNAKE +,,SNAKY +,,SNAPS +,,SNARE +,,SNARF +,,SNARK +,,SNARL +,,SNEAK +,,SNEER +,,SNICK +,,SNIDE +,,SNIFF +,,SNIPE +,,SNIPS +,,SNITS +,,SNOBS +,,SNOGS +,,SNOOD +,,SNOOK +,,SNOOP +,,SNOOT +,,SNORE +,,SNORT +,,SNOTS +,,SNOUT +,,SNOWS +,,SNOWY +,,SNUBS +,,SNUCK +,,SNUFF +,,SNUGS +,,SOAKS +,,SOAPS +,,SOAPY +,,SOARS +,,SOBER +,,SOCKS +,,SODAS +,,SOFAS +,,SOFTY +,,SOGGY +,,SOILS +,,SOLAR +,,SOLED +,,SOLEI +,,SOLES +,,SOLID +,,SOLOS +,,SOLVE +,,SONAR +,,SONGS +,,SONIC +,,SONNY +,,SOOKS +,,SOOTH +,,SOOTY +,,SOPPY +,,SORER +,,SORES +,,SORRY +,,SORTA +,,SORTS +,,SOUGH +,,SOUKS +,,SOULS +,,SOUND +,,SOUPS +,,SOUPY +,,SOURS +,,SOUSE +,,SOUTH +,,SOWED +,,SOWER +,,SPACE +,,SPACY +,,SPADE +,,SPAKE +,,SPAMS +,,SPANK +,,SPANS +,,SPARE +,,SPARK +,,SPARS +,,SPASM +,,SPATE +,,SPATS +,,SPAWN +,,SPAYS +,,SPAZA +,,SPEAK +,,SPEAR +,,SPECK +,,SPECS +,,SPECT +,,SPEED +,,SPELL +,,SPELT +,,SPEND +,,SPENT +,,SPERM +,,SPEWS +,,SPICA +,,SPICE +,,SPICY +,,SPIED +,,SPIEL +,,SPIES +,,SPIFF +,,SPIKE +,,SPIKY +,,SPILE +,,SPILL +,,SPILT +,,SPINE +,,SPINS +,,SPIRE +,,SPITE +,,SPITS +,,SPITZ +,,SPIVS +,,SPLAT +,,SPLAY +,,SPLIT +,,SPODS +,,SPOIL +,,SPOKE +,,SPOOF +,,SPOOK +,,SPOOL +,,SPOON +,,SPOOR +,,SPORE +,,SPORT +,,SPOTS +,,SPOUT +,,SPRAT +,,SPRAY +,,SPREE +,,SPRIG +,,SPRIT +,,SPROG +,,SPRUE +,,SPUDS +,,SPUME +,,SPUNK +,,SPURN +,,SPURS +,,SPURT +,,SQUAB +,,SQUAD +,,SQUAT +,,SQUIB +,,SQUID +,,STABS +,,STACK +,,STAFF +,,STAGE +,,STAGS +,,STAGY +,,STAID +,,STAIN +,,STAIR +,,STAKE +,,STALE +,,STALK +,,STALL +,,STAMP +,,STAND +,,STANK +,,STAPH +,,STARE +,,STARK +,,STARS +,,START +,,STASH +,,STATE +,,STATS +,,STAVE +,,STAYS +,,STEAD +,,STEAK +,,STEAL +,,STEAM +,,STEED +,,STEEL +,,STEEP +,,STEER +,,STEIN +,,STELE +,,STEMS +,,STENO +,,STENT +,,STEPS +,,STERN +,,STEWS +,,STICK +,,STIES +,,STIFF +,,STILE +,,STILL +,,STILT +,,STING +,,STINK +,,STINT +,,STIRS +,,STOAT +,,STOCK +,,STOEP +,,STOIC +,,STOKE +,,STOLE +,,STOMA +,,STOMP +,,STONE +,,STONY +,,STOOD +,,STOOL +,,STOOP +,,STOPE +,,STOPS +,,STORE +,,STORK +,,STORM +,,STORY +,,STOUP +,,STOUT +,,STOVE +,,STOWS +,,STRAP +,,STRAW +,,STRAY +,,STREP +,,STREW +,,STRIA +,,STRIP +,,STROP +,,STRUM +,,STRUT +,,STUBS +,,STUCK +,,STUDS +,,STUDY +,,STUFF +,,STUMP +,,STUNG +,,STUNK +,,STUNS +,,STUNT +,,STUPA +,,STYES +,,STYLE +,,SUAVE +,,SUCKS +,,SUCKY +,,SUCRE +,,SUEDE +,,SUGAR +,,SUING +,,SUITE +,,SUITS +,,SULCI +,,SULKS +,,SULKY +,,SULLY +,,SUMAC +,,SUMMA +,,SUMPS +,,SUNNY +,,SUNUP +,,SUPER +,,SUPRA +,,SURAL +,,SURER +,,SURFS +,,SURGE +,,SURLY +,,SUSHI +,,SWABS +,,SWAGE +,,SWAGS +,,SWAIN +,,SWALE +,,SWAMI +,,SWAMP +,,SWANK +,,SWAPS +,,SWARD +,,SWARF +,,SWARM +,,SWASH +,,SWATH +,,SWATS +,,SWAYS +,,SWEAR +,,SWEAT +,,SWEDE +,,SWEEP +,,SWEET +,,SWELL +,,SWEPT +,,SWIFT +,,SWIGS +,,SWILL +,,SWIMS +,,SWINE +,,SWING +,,SWIPE +,,SWIRL +,,SWISH +,,SWISS +,,SWIZZ +,,SWOON +,,SWOOP +,,SWOPS +,,SWORD +,,SWORE +,,SWORN +,,SWOTS +,,SWUNG +,,SYNCH +,,SYNCS +,,SYNOD +,,SYNTH +,,SYRUP +,,TABBY +,,TABLA +,,TABLE +,,TABOO +,,TACHE +,,TACIT +,,TACKS +,,TACKY +,,TACOS +,,TAFFY +,,TAIGA +,,TAILS +,,TAINT +,,TAKEN +,,TAKER +,,TAKES +,,TALES +,,TALKS +,,TALKY +,,TALLY +,,TALON +,,TALUS +,,TAMED +,,TAMER +,,TAMES +,,TAMPS +,,TANGO +,,TANGY +,,TANKA +,,TANKS +,,TANSY +,,TAPAS +,,TAPED +,,TAPER +,,TAPES +,,TAPIR +,,TARDY +,,TARNS +,,TAROS +,,TAROT +,,TARPS +,,TARRY +,,TARSI +,,TARTS +,,TARTY +,,TASED +,,TASER +,,TASES +,,TASKS +,,TASTE +,,TASTY +,,TATER +,,TATTS +,,TATTY +,,TAUNT +,,TAUPE +,,TAWNY +,,TAXED +,,TAXES +,,TAXIS +,,TAXON +,,TEACH +,,TEALS +,,TEAMS +,,TEARS +,,TEARY +,,TEASE +,,TEATS +,,TECHS +,,TECHY +,,TEDDY +,,TEEMS +,,TEENS +,,TEENY +,,TEETH +,,TELCO +,,TELEX +,,TELLS +,,TELLY +,,TEMPI +,,TEMPO +,,TEMPS +,,TEMPT +,,TENDS +,,TENET +,,TENON +,,TENOR +,,TENSE +,,TENTH +,,TENTS +,,TEPEE +,,TEPID +,,TERMS +,,TERNS +,,TERRA +,,TERRY +,,TERSE +,,TESLA +,,TESTA +,,TESTS +,,TESTY +,,TEXTS +,,THANE +,,THANG +,,THANK +,,THAWS +,,THEES +,,THEFT +,,THEIR +,,THEME +,,THERE +,,THERM +,,THESE +,,THETA +,,THEWS +,,THICK +,,THIEF +,,THIGH +,,THINE +,,THING +,,THINK +,,THINS +,,THIRD +,,THONG +,,THORN +,,THOSE +,,THREE +,,THREW +,,THROB +,,THROW +,,THRUM +,,THUDS +,,THUGS +,,THUJA +,,THUMB +,,THUMP +,,THUNK +,,THYME +,,TIARA +,,TIBIA +,,TICKS +,,TIDAL +,,TIDED +,,TIDES +,,TIERS +,,TIFFS +,,TIGER +,,TIGHT +,,TIKES +,,TIKKA +,,TILDE +,,TILED +,,TILER +,,TILES +,,TILLS +,,TILTS +,,TIMED +,,TIMER +,,TIMES +,,TIMID +,,TINEA +,,TINES +,,TINGE +,,TINGS +,,TINNY +,,TINTS +,,TIPIS +,,TIPPY +,,TIPSY +,,TIRED +,,TIRES +,,TITAN +,,TITCH +,,TITER +,,TITHE +,,TITLE +,,TITRE +,,TITTY +,,TIZZY +,,TOADS +,,TOADY +,,TOAST +,,TODAY +,,TODDY +,,TOFFS +,,TOGAS +,,TOILS +,,TOKED +,,TOKEN +,,TOKES +,,TOLLS +,,TOMBS +,,TOMES +,,TOMMY +,,TONAL +,,TONED +,,TONER +,,TONES +,,TONGS +,,TONIC +,,TONKS +,,TONNE +,,TOOLS +,,TOOTH +,,TOOTS +,,TOPAZ +,,TOPEE +,,TOPIC +,,TOQUE +,,TORCH +,,TORSO +,,TORTE +,,TORTS +,,TORUS +,,TOTAL +,,TOTED +,,TOTEM +,,TOTES +,,TOTTY +,,TOUCH +,,TOUGH +,,TOURS +,,TOUTS +,,TOWED +,,TOWEL +,,TOWER +,,TOWNS +,,TOXIC +,,TOXIN +,,TOYED +,,TOYON +,,TRACE +,,TRACK +,,TRACT +,,TRADE +,,TRAIL +,,TRAIN +,,TRAIT +,,TRAMP +,,TRAMS +,,TRANQ +,,TRANS +,,TRAPS +,,TRASH +,,TRAWL +,,TRAYS +,,TREAD +,,TREAT +,,TREES +,,TREKS +,,TREND +,,TRESS +,,TREWS +,,TRIAD +,,TRIAL +,,TRIBE +,,TRICE +,,TRICK +,,TRIDE +,,TRIED +,,TRIER +,,TRIES +,,TRIKE +,,TRILL +,,TRIMS +,,TRIOS +,,TRIPE +,,TRIPS +,,TRITE +,,TROLL +,,TROMP +,,TRONC +,,TROOP +,,TROPE +,,TROTH +,,TROTS +,,TROUT +,,TROVE +,,TRUCE +,,TRUCK +,,TRUER +,,TRUGS +,,TRULY +,,TRUMP +,,TRUNK +,,TRUSS +,,TRUST +,,TRUTH +,,TRYST +,,TSARS +,,TUBAL +,,TUBAS +,,TUBBY +,,TUBED +,,TUBER +,,TUBES +,,TUCKS +,,TUFTS +,,TULES +,,TULIP +,,TULLE +,,TUMID +,,TUMMY +,,TUMOR +,,TUNAS +,,TUNED +,,TUNER +,,TUNES +,,TUNIC +,,TUPLE +,,TUQUE +,,TURBO +,,TURDS +,,TURFS +,,TURNS +,,TURPS +,,TUSKS +,,TUTOR +,,TUTTI +,,TUTUS +,,TWAIN +,,TWANG +,,TWATS +,,TWEAK +,,TWEED +,,TWEEN +,,TWEET +,,TWERK +,,TWERP +,,TWICE +,,TWIGS +,,TWILL +,,TWINE +,,TWINK +,,TWINS +,,TWIRL +,,TWIRP +,,TWIST +,,TWIXT +,,TYING +,,TYKES +,,TYPED +,,TYPES +,,TYPOS +,,TYRES +,,TYROS +,,TZARS +,,UDDER +,,ULCER +,,ULNAE +,,ULNAS +,,ULTRA +,,UMAMI +,,UMBEL +,,UMBER +,,UMBOS +,,UMBRA +,,UNBAN +,,UNBOX +,,UNCAP +,,UNCLE +,,UNCUT +,,UNDER +,,UNDID +,,UNDUE +,,UNFED +,,UNFIT +,,UNHIP +,,UNIFY +,,UNION +,,UNITE +,,UNITS +,,UNITY +,,UNLIT +,,UNMET +,,UNSEE +,,UNSET +,,UNTIE +,,UNTIL +,,UNWED +,,UNZIP +,,UPDOS +,,UPEND +,,UPLIT +,,UPPED +,,UPPER +,,UPSET +,,URBAN +,,URGED +,,URGES +,,URINE +,,USAGE +,,USERS +,,USHER +,,USING +,,USUAL +,,USURP +,,USURY +,,UTERI +,,UTTER +,,UVEAS +,,UVULA +,,VACAY +,,VAGAL +,,VAGUE +,,VALES +,,VALET +,,VALID +,,VALOR +,,VALSE +,,VALUE +,,VALVE +,,VAMPS +,,VANES +,,VAPED +,,VAPER +,,VAPES +,,VAPID +,,VAPOR +,,VARIX +,,VARUS +,,VASES +,,VAULT +,,VAUNT +,,VEENA +,,VEEPS +,,VEERS +,,VEGAN +,,VEGES +,,VEILS +,,VEINS +,,VELAR +,,VELDT +,,VELUM +,,VENAL +,,VENDS +,,VENOM +,,VENTS +,,VENUE +,,VERBS +,,VERGE +,,VERSE +,,VERSO +,,VERTS +,,VERVE +,,VESTA +,,VESTS +,,VETCH +,,VEXED +,,VEXES +,,VIALS +,,VIBED +,,VIBES +,,VICAR +,,VICES +,,VIDEO +,,VIEWS +,,VIGIL +,,VIGOR +,,VILER +,,VILLA +,,VILLI +,,VINES +,,VINYL +,,VIOLA +,,VIOLS +,,VIPER +,,VIRAL +,,VIRUS +,,VISAS +,,VISES +,,VISIT +,,VISOR +,,VISTA +,,VITAL +,,VITRO +,,VIVAS +,,VIVID +,,VIXEN +,,VLOGS +,,VOCAL +,,VODKA +,,VOGUE +,,VOICE +,,VOIDS +,,VOILA +,,VOILE +,,VOLAR +,,VOLES +,,VOLTE +,,VOLTS +,,VOMER +,,VOMIT +,,VOTED +,,VOTER +,,VOTES +,,VOUCH +,,VOWED +,,VOWEL +,,VROOM +,,VULVA +,,VYING +,,WACKO +,,WACKY +,,WADDY +,,WADED +,,WADER +,,WADES +,,WADIS +,,WAFER +,,WAFTS +,,WAGED +,,WAGER +,,WAGES +,,WAGON +,,WAGYU +,,WAHOO +,,WAIFS +,,WAILS +,,WAINS +,,WAIST +,,WAITS +,,WAIVE +,,WAKED +,,WAKEN +,,WAKES +,,WALKS +,,WALLS +,,WALLY +,,WALTZ +,,WANDS +,,WANED +,,WANES +,,WANTS +,,WARDS +,,WARES +,,WARMS +,,WARNS +,,WARPS +,,WARTS +,,WARTY +,,WASHY +,,WASPS +,,WASPY +,,WASTE +,,WATCH +,,WATER +,,WATTS +,,WAVED +,,WAVER +,,WAVES +,,WAXED +,,WAXEN +,,WAXES +,,WAZOO +,,WEALS +,,WEARS +,,WEARY +,,WEAVE +,,WEDGE +,,WEEDS +,,WEEDY +,,WEEKS +,,WEENY +,,WEEPS +,,WEEPY +,,WEIGH +,,WEIRD +,,WELCH +,,WELDS +,,WELLS +,,WELLY +,,WELSH +,,WELTS +,,WENCH +,,WETAS +,,WETLY +,,WHACK +,,WHALE +,,WHANG +,,WHARF +,,WHEAT +,,WHEEL +,,WHELK +,,WHELM +,,WHELP +,,WHERE +,,WHETS +,,WHICH +,,WHIFF +,,WHILE +,,WHIMS +,,WHINE +,,WHINY +,,WHIPS +,,WHIRL +,,WHIRR +,,WHIRS +,,WHISK +,,WHIST +,,WHITE +,,WHIZZ +,,WHOLE +,,WHOMP +,,WHOOP +,,WHOPS +,,WHORE +,,WHORL +,,WHOSE +,,WHUPS +,,WICKS +,,WIDEN +,,WIDER +,,WIDES +,,WIDOW +,,WIDTH +,,WIELD +,,WIGHT +,,WIKIS +,,WILCO +,,WILES +,,WILLS +,,WILLY +,,WILTS +,,WIMPS +,,WIMPY +,,WINCE +,,WINCH +,,WINDS +,,WINDY +,,WINED +,,WINES +,,WINGS +,,WINKS +,,WINOS +,,WIPED +,,WIPER +,,WIPES +,,WIRED +,,WIRES +,,WISED +,,WISER +,,WISES +,,WISPS +,,WISPY +,,WITCH +,,WITTY +,,WIVES +,,WODGE +,,WOKEN +,,WOLDS +,,WOLFS +,,WOMAN +,,WOMBS +,,WOMEN +,,WONGA +,,WONKS +,,WONKY +,,WOODS +,,WOODY +,,WOOED +,,WOOFS +,,WOOLY +,,WOOZY +,,WORDS +,,WORDY +,,WORKS +,,WORLD +,,WORMS +,,WORMY +,,WORRY +,,WORSE +,,WORST +,,WORTH +,,WOULD +,,WOUND +,,WOVEN +,,WOWED +,,WRACK +,,WRAPS +,,WRATH +,,WREAK +,,WRECK +,,WRENS +,,WREST +,,WRING +,,WRIST +,,WRITE +,,WRITS +,,WRONG +,,WROTE +,,WRUNG +,,WRYLY +,,WURST +,,WUSHU +,,XENON +,,XEROX +,,XYLEM +,,YACHT +,,YAHOO +,,YAKKA +,,YANKS +,,YAPPY +,,YARDS +,,YARNS +,,YAWED +,,YAWNS +,,YEARN +,,YEARS +,,YEAST +,,YECCH +,,YELLS +,,YELPS +,,YERBA +,,YESES +,,YETIS +,,YIELD +,,YIKES +,,YOBBO +,,YODEL +,,YOGIS +,,YOKED +,,YOKEL +,,YOKES +,,YOLKS +,,YOMPS +,,YONKS +,,YOUNG +,,YOURS +,,YOUSE +,,YOUTH +,,YOWLS +,,YUCCA +,,YUCKY +,,YUKKY +,,YUMMY +,,YUPPY +,,YURTS +,,YUZUS +,,ZAKAT +,,ZAPPY +,,ZEBRA +,,ZEROS +,,ZETAS +,,ZILCH +,,ZINES +,,ZIPPY +,,ZLOTY +,,ZONAL +,,ZONED +,,ZONES ,,ZOOMS \ No newline at end of file diff --git a/reasoning_gym/games/__init__.py b/reasoning_gym/games/__init__.py index cf083ba4..8e4e32d6 100644 --- a/reasoning_gym/games/__init__.py +++ b/reasoning_gym/games/__init__.py @@ -3,12 +3,16 @@ - Board games - Puzzle games - Strategy games +- Simulation games """ from .countdown import CountdownConfig, CountdownDataset +from .game_of_life import GameOfLifeConfig, GameOfLifeDataset from .maze import MazeConfig, MazeDataset from .mini_sudoku import MiniSudokuConfig, MiniSudokuDataset +from .n_queens import NQueensDataset from .sudoku import SudokuConfig, SudokuDataset +from .tower_of_hanoi import HanoiConfig, HanoiDataset __all__ = [ "CountdownConfig", @@ -19,4 +23,9 @@ "SudokuDataset", "MazeConfig", "MazeDataset", + "GameOfLifeConfig", + "GameOfLifeDataset", + "HanoiConfig", + "HanoiDataset", + "NQueensDataset", ] diff --git a/reasoning_gym/games/countdown.py b/reasoning_gym/games/countdown.py index 4721844d..38a60c4f 100644 --- a/reasoning_gym/games/countdown.py +++ b/reasoning_gym/games/countdown.py @@ -1,9 +1,10 @@ from dataclasses import dataclass from random import Random -from typing import List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import sympy from sympy import Symbol, symbols +from sympy.parsing.sympy_parser import parse_expr from ..factory import ProceduralDataset, register_dataset @@ -158,6 +159,23 @@ def _generate_expression(self, rng: Random) -> Tuple[str, List[int], int]: raise ValueError(f"Failed to generate valid expression after {max_attempts} attempts") + def score_answer(self, answer: Optional[str], metadata: Dict[str, Any]) -> float: + """Determine if the solution provided solves the problem""" + reward = 0.0 + if answer is not None: + try: + user_answer = int(parse_expr(answer)) + solved = user_answer == metadata["target"] + if solved: + reward = 1.0 + elif len(answer.strip()) > 0: # encourage partial solutions + reward = 0.05 + else: + reward = 0.01 + except: + reward = 0.01 + return reward + # Register the dataset register_dataset("countdown", CountdownDataset, CountdownConfig) diff --git a/reasoning_gym/games/game_of_life.py b/reasoning_gym/games/game_of_life.py new file mode 100644 index 00000000..c8cdc0d1 --- /dev/null +++ b/reasoning_gym/games/game_of_life.py @@ -0,0 +1,102 @@ +from dataclasses import dataclass +from random import Random +from typing import Dict, List, Optional, Tuple + +import cellpylib as cpl + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class GameOfLifeConfig: + """Configuration for sudoku puzzle generation""" + + grid_size_x: int = 20 + grid_size_y: int = 20 + filled_cells: int = 100 # actually a max + simulation_steps: int = 1 + seed: Optional[int] = None + size: int = 500 + + def validate(self): + """Validate configuration parameters""" + assert 3 <= self.grid_size_x <= 999, "grid_size_x must be between 0 and 999" + assert 3 <= self.grid_size_y <= 999, "grid_size_y must be between 0 and 999" + assert self.simulation_steps >= 0, "simulation_steps must be gte 0" + assert self.filled_cells <= self.grid_size_x * self.grid_size_y, "filled_cells must fit in x times y" + + +class GameOfLifeDataset(ProceduralDataset): + """Generates Game of Life games with configurable parameters""" + + def __init__(self, config: GameOfLifeConfig): + self._prompt_templates = [ + "What will this Game of Life board look like after {simulation_steps} steps of simulation?\n\n{board}" + ] + + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """Generate a single GameOfLife task + + Returns: + dict with keys: + - question: str, the task description + - answer: str, a solution string + - metadata: dict with generation parameters + """ + rng = Random(self.seed + idx) + + # Make the board + board = cpl.init_simple2d(self.config.grid_size_x, self.config.grid_size_y) + board[:, :, :] = 0 + + # Add the cells + for i in range(0, self.config.filled_cells): + rx = rng.randint(0, self.config.grid_size_x - 1) + ry = rng.randint(0, self.config.grid_size_y - 1) + board[:, rx, ry] = 1 + + # Simulate the result to get the answer + evolved = cpl.evolve2d( + board, timesteps=self.config.simulation_steps + 1, apply_rule=cpl.game_of_life_rule, memoize="recursive" + ) + + board_str = str(board[0]) + result_str = str(evolved[-1]) + + return { + "question": rng.choice(self._prompt_templates).format( + simulation_steps=self.config.simulation_steps, board=board_str + ), + "answer": result_str, + "metadata": { + "grid_size_x": self.config.grid_size_x, + "grid_size_y": self.config.grid_size_y, + "filled_cells": self.config.filled_cells, + "simulation_steps": self.config.simulation_steps, + }, + } + + def score_answer(self, answer: Optional[str], entry: Dict[str, any]) -> float: + """Determine if the solution provided solves the GoL task. + + The function awards 1.0 for a correct answer. + + Args: + answer (Optional[str]): The user's answer. + entry (Dict[str, any]): The original dataset entry containing the correct answer. + + Returns: + float: The computed score between 0.0 and 1.0. + """ + + if answer == None: + return 0.0 + if answer.replace("\n", "") != entry["answer"].replace("\n", ""): + return 0.01 + else: + return 1.0 # Yay + + +register_dataset("game_of_life", GameOfLifeDataset, GameOfLifeConfig) diff --git a/reasoning_gym/games/maze.py b/reasoning_gym/games/maze.py index 2c8cd9bd..cc110f42 100644 --- a/reasoning_gym/games/maze.py +++ b/reasoning_gym/games/maze.py @@ -90,9 +90,9 @@ def __getitem__(self, idx: int) -> dict: # Maze is good, build the question question_str = ( f"Navigate from '{self.start_char}' (start) to '{self.goal_char}' (goal):\n\n" - + "```" + + "```\n" + self._maze_to_str(maze_grid) - + "```" + + "\n```" + "\nLegend: " + f"'{self.wall_char}' = Wall, '{self.path_char}' = Passage\n\n" + "What is the minimum number of steps to reach the goal?" diff --git a/reasoning_gym/games/n_queens.py b/reasoning_gym/games/n_queens.py new file mode 100644 index 00000000..1fef6c62 --- /dev/null +++ b/reasoning_gym/games/n_queens.py @@ -0,0 +1,163 @@ +"""N Queens puzzle generator + +A generalization of the 8-queens puzzle to any board size. +https://en.wikipedia.org/wiki/Eight_queens_puzzle +""" + +from copy import deepcopy +from dataclasses import dataclass +from random import Random +from typing import Dict, List, Optional + +from ..factory import ProceduralDataset, register_dataset + +MIN_BOARD_SIZE = 4 +MAX_BOARD_SIZE = 12 + +QUESTION_TEMPLATE = """Solve this N Queens puzzle: +{puzzle} + +The board size is {n}x{n} and your job is to place {num_removed} queen(s) on the board such that no two queens attack each other. + +No two queens attack each other if they are not in the same row, column, or diagonal. + +Place a queen by replacing an underscore (_) with a Q. +""" + + +@dataclass +class NQueensConfig: + """Configuration for N Queens puzzle generation""" + + n: int = 8 # Board size + min_remove: int = 1 # Minimum number of queens to remove from solved board + max_remove: int = 7 # Maximum number of queens to remove from solved board + + size: int = 500 # Virtual dataset size + seed: Optional[int] = None + + def validate(self): + """Validate configuration parameters""" + assert MIN_BOARD_SIZE <= self.n <= MAX_BOARD_SIZE, f"n must be between {MIN_BOARD_SIZE} and {MAX_BOARD_SIZE}" + assert 1 <= self.min_remove <= self.max_remove, "min_remove must be between 1 and max_remove" + assert self.min_remove <= self.max_remove <= self.n, "max_remove must be between min_remove and n" + + +class NQueensDataset(ProceduralDataset): + """Generates N Queens puzzles with configurable difficulty""" + + def __init__(self, config: NQueensConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + self._solutions = self._get_all_solutions(config.n) + + def __len__(self) -> int: + return self.config.size + + def __iter__(self): + self._current_idx = 0 + return self + + def __next__(self): + if self._current_idx >= self.config.size: + raise StopIteration + item = self[self._current_idx] + self._current_idx += 1 + return item + + def _get_all_solutions(self, n: int) -> List[List[List[str]]]: + """Get all solutions for the N Queens puzzle""" + + visited_cols = set() + visited_pos_diag = set() + visited_neg_diag = set() + + res = [] + board = [["_"] * n for _ in range(n)] + + def backtrack(row: int): + if row == n: + res.append(deepcopy(board)) + return + + for col in range(n): + if col in visited_cols or (row + col) in visited_pos_diag or (row - col) in visited_neg_diag: + continue + + visited_cols.add(col) + visited_pos_diag.add(row + col) + visited_neg_diag.add(row - col) + board[row][col] = "Q" + backtrack(row + 1) + visited_cols.remove(col) + visited_pos_diag.remove(row + col) + visited_neg_diag.remove(row - col) + board[row][col] = "_" + + backtrack(0) + return res + + def _create_puzzle(self, solved_board: List[List[str]], num_removed: int, rng: Random) -> List[List[str]]: + """Create puzzle by removing queens from solved board""" + puzzle = deepcopy(solved_board) + queens = [(i, j) for i in range(len(puzzle)) for j in range(len(puzzle)) if puzzle[i][j] == "Q"] + rng.shuffle(queens) + for i in range(num_removed): + x, y = queens[i] + puzzle[x][y] = "_" + return puzzle + + def _board_to_string(self, board: List[List[str]]) -> str: + """Convert board to string representation""" + return "\n".join(" ".join(x for x in row) for row in board) + + def _string_to_board(self, board_str: str) -> List[List[str]]: + """Convert string representation to board""" + return [list(row.split()) for row in board_str.strip().split("\n")] + + def _is_tractable_solution(self, puzzle: List[List[str]], solution: List[List[str]]) -> bool: + """Check if a solution is achievable from the starting state of the puzzle""" + for r in range(len(puzzle)): + for c in range(len(puzzle)): + if puzzle[r][c] == "Q" and solution[r][c] != "Q": + return False + return True + + def __getitem__(self, idx: int) -> dict: + """Generate a single N Queens puzzle""" + rng = Random(self.seed + idx) + + # Randomly select a valid solution + solved_board = rng.choice(self._solutions) + + # Create puzzle by removing queens + num_removed = rng.randint(self.config.min_remove, self.config.max_remove) + puzzle = self._create_puzzle(solved_board, num_removed, rng) + puzzle_str = self._board_to_string(puzzle) + + # Filter all solutions that are intractable from the puzzle's starting state + valid_solutions = [board for board in self._solutions if self._is_tractable_solution(puzzle, board)] + valid_solutions_str = sorted({self._board_to_string(board) for board in valid_solutions}) + + return { + "question": QUESTION_TEMPLATE.format(puzzle=puzzle_str, n=len(puzzle), num_removed=num_removed), + "answer": rng.choice(valid_solutions_str), # choose arbitary answer (e.g. for SFT) + "metadata": { + "puzzle": puzzle, + "solutions": valid_solutions, + "num_removed": num_removed, + "valid_answers": valid_solutions_str, + }, + } + + def score_answer(self, answer: Optional[str], entry: Dict[str, any]) -> float: + valid_solutions = entry["metadata"]["valid_answers"] + reward = 0.0 + if answer is not None: + if answer in valid_solutions: + reward = 1.0 + else: + reward = 0.01 + return reward + + +register_dataset("n_queens", NQueensDataset, NQueensConfig) diff --git a/reasoning_gym/games/tower_of_hanoi.py b/reasoning_gym/games/tower_of_hanoi.py new file mode 100644 index 00000000..df902300 --- /dev/null +++ b/reasoning_gym/games/tower_of_hanoi.py @@ -0,0 +1,373 @@ +# reasoning_gym/games/tower_of_hanoi.py + +import math +import random +import re +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class HanoiConfig: + """ + Configuration for the Tower of Hanoi task. + + - min_disks: Minimum number of disks in the puzzle. + - max_disks: Maximum number of disks in the puzzle. + - min_pegs: Minimum number of pegs (minimum 3). + - max_pegs: Maximum number of pegs. + - size: Number of problem instances in the dataset. + - seed: Optional seed for reproducibility. + - visualize: Whether to include a visualization of the initial state. + """ + + min_disks: int = 3 + max_disks: int = 7 + min_pegs: int = 3 + max_pegs: int = 4 + size: int = 50 + seed: Optional[int] = None + visualize: bool = False # New parameter + + def validate(self) -> None: + """Validate configuration parameters.""" + assert self.min_disks >= 1, "min_disks must be at least 1" + assert self.max_disks >= self.min_disks, "max_disks must be >= min_disks" + assert self.min_pegs >= 3, "min_pegs must be at least 3" + assert self.max_pegs >= self.min_pegs, "max_pegs must be >= min_pegs" + + +class MoveGenerator: + """ + Helper class to generate valid move sequences for Tower of Hanoi using the Frame-Stewart algorithm. + It maintains the current state of all pegs to ensure move validity. + """ + + def __init__(self, num_disks: int, pegs: List[int], start: int, target: int): + self.num_disks = num_disks + self.pegs = pegs + self.start = start + self.target = target + self.auxiliary_pegs = [peg for peg in pegs if peg not in (start, target)] + self.pegs_state: Dict[int, List[int]] = {peg: [] for peg in pegs} + for disk in range(num_disks, 0, -1): # Largest disk at the bottom + self.pegs_state[start].append(disk) + self.moves: List[str] = [] + self.memo: Dict[Tuple[int, int], int] = {} # Memoization for T(n, k) + + def generate_moves(self) -> List[str]: + self.move(n=self.num_disks, source=self.start, target=self.target, auxiliary_pegs=self.auxiliary_pegs) + return self.moves + + def move(self, n: int, source: int, target: int, auxiliary_pegs: List[int]): + if n == 0: + return + if n == 1: + self._move_disk(source, target) + return + + k = len(auxiliary_pegs) + 2 # Total number of pegs including source and target + + if k < 3: + raise ValueError("At least 3 pegs are required.") + + if k == 3: + # Classic Tower of Hanoi solution + aux = auxiliary_pegs[0] + self.move(n - 1, source, aux, [target]) + self._move_disk(source, target) + self.move(n - 1, aux, target, [source]) + return + + # For k > 3, apply Frame-Stewart algorithm + # Find m that minimizes 2*T(m, k) + T(n - m, k - 1) + min_moves = math.inf + best_m = 1 + for m in range(1, n): + moves_m = self._compute_T(m, k) + moves_n_minus_m = self._compute_T(n - m, k - 1) + total_moves = 2 * moves_m + moves_n_minus_m + if total_moves < min_moves: + min_moves = total_moves + best_m = m + + # Select a temporary peg to hold m disks + temp_peg = auxiliary_pegs[0] + new_auxiliary = [peg for peg in auxiliary_pegs if peg != temp_peg] + + # Step 1: Move top m disks to temp_peg using all pegs + self.move(n=best_m, source=source, target=temp_peg, auxiliary_pegs=auxiliary_pegs[1:] + [target]) + + # Step 2: Move remaining n - m disks to target using k - 1 pegs + self.move(n=n - best_m, source=source, target=target, auxiliary_pegs=new_auxiliary) + + # Step 3: Move m disks from temp_peg to target using all pegs + self.move(n=best_m, source=temp_peg, target=target, auxiliary_pegs=auxiliary_pegs[1:] + [source]) + + def _move_disk(self, from_peg: int, to_peg: int): + if not self.pegs_state[from_peg]: + raise ValueError(f"No disks to move from Peg {from_peg}.") + disk = self.pegs_state[from_peg][-1] + self.pegs_state[from_peg].pop() + self.pegs_state[to_peg].append(disk) + self.moves.append(f"Move disk {disk} from Peg {from_peg} to Peg {to_peg}") + + def _compute_T(self, n: int, k: int) -> int: + """ + Compute the minimal number of moves (T(n, k)) required to move n disks using k pegs. + Utilizes memoization to store previously computed results. + """ + if n == 0: + return 0 + if n == 1: + return 1 + if k == 3: + return 2**n - 1 + if (n, k) in self.memo: + return self.memo[(n, k)] + + min_moves = math.inf + for m in range(1, n): + moves = 2 * self._compute_T(m, k) + self._compute_T(n - m, k - 1) + if moves < min_moves: + min_moves = moves + self.memo[(n, k)] = min_moves + return min_moves + + +class HanoiDataset(ProceduralDataset): + """ + Generates Tower of Hanoi problems with solutions. + Supports variable number of pegs using the optimized Frame-Stewart algorithm with Peg State Tracking. + """ + + def __init__(self, config: HanoiConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + self.min_pegs = config.min_pegs + self.max_pegs = config.max_pegs + self.min_disks = config.min_disks + self.max_disks = config.max_disks + self.visualize = config.visualize # Initialize the visualize attribute + + def __getitem__(self, idx: int) -> dict: + """ + Generate a Tower of Hanoi problem instance. + + Returns: + dict with: + - "question": Text describing the problem setup. + - "answer": List of moves to solve the puzzle. + - "metadata": Configuration and solution details. + - "initial_state": (Optional) ASCII visualization of the initial pegs. + - "states": (Optional) List of ASCII visualizations after each move. + """ + rng = random.Random(self.seed + idx if self.seed is not None else None) + + # Randomly select number of disks and pegs within the specified ranges + num_disks = rng.randint(self.min_disks, self.max_disks) + num_pegs = rng.randint(self.min_pegs, self.max_pegs) + + # Assign unique peg identifiers (e.g., integers starting from 1) + pegs = list(range(1, num_pegs + 1)) + + """ #Debug: Print current instance configuration + print(f"\n--- Generating Instance {idx} ---") + print(f"Number of Disks: {num_disks}") + print(f"Number of Pegs: {num_pegs}") + print(f"Pegs: {pegs}") + """ + + # Randomly select start and target pegs + start_peg, target_peg = rng.sample(pegs, 2) + + # Auxiliary pegs are the remaining pegs + auxiliary_pegs = [peg for peg in pegs if peg not in (start_peg, target_peg)] + + """ # Debug: Print start, target, and auxiliary pegs + print(f"Start Peg: {start_peg}") + print(f"Target Peg: {target_peg}") + print(f"Auxiliary Pegs: {auxiliary_pegs}") + """ + + # Initialize the MoveGenerator and generate moves + move_gen = MoveGenerator(num_disks, pegs, start_peg, target_peg) + try: + solution = move_gen.generate_moves() + except ValueError as ve: + # print(f"Error during move generation: {ve}") + raise ve + + """ # Debug: Print the solution moves + print(f"Solution Length: {len(solution)}") + print("Solution Moves:") + for move_num, move in enumerate(solution, start=1): + print(f" Move {move_num}: {move}") + """ + + # Initialize pegs_state: all disks start on the start peg + pegs_state = {peg: [] for peg in pegs} + for disk in range(num_disks, 0, -1): # Largest disk at the bottom + pegs_state[start_peg].append(disk) + + # Generate initial state visualization if requested + initial_state_str = None + if self.visualize: + initial_state_str = self._visualize_state(pegs_state) + + # Apply moves to track state changes + states = [] + if self.visualize: + states.append(initial_state_str) # Initial state + for move in solution: + # Parse the move string using regex + try: + disk, from_peg, to_peg = self._parse_move(move) + except ValueError as ve: + # print(f"Error parsing move: {ve}") + raise ve + + # Validate the move + if not self._validate_move(pegs_state, move): + # print(f"Invalid move detected: {move}") + # print(f"Current Pegs State: {pegs_state}") + raise ValueError(f"Invalid move detected: {move}") + + # Move the disk + pegs_state[from_peg].pop() + pegs_state[to_peg].append(disk) + + # Visualize the new state + new_state_str = self._visualize_state(pegs_state) + states.append(new_state_str) + + # Peg labels + peg_labels = {peg: f"Peg {peg}" for peg in pegs} + + question_str = ( + f"Solve the Tower of Hanoi problem with {num_disks} disks and {num_pegs} pegs.\n" + f"Move all disks from {peg_labels[start_peg]} to {peg_labels[target_peg]} following the rules:\n" + "- Only one disk can be moved at a time.\n" + "- A larger disk cannot be placed on top of a smaller disk.\n" + "- All disks must be on a peg at all times.\n" + "Example:\n" + "Move disk 1 from Peg 1 to Peg 3\n" + "Move disk 2 from Peg 1 to Peg 2\n" + "Move disk 1 from Peg 3 to Peg 2\n" + "\n" + "Provide the sequence of moves." + ) + + result = { + "question": question_str, + "answer": solution, + "metadata": { + "num_disks": num_disks, + "num_pegs": num_pegs, + "start_peg": start_peg, + "target_peg": target_peg, + "auxiliary_pegs": auxiliary_pegs, + "solution_length": len(solution), + }, + } + + if self.visualize: + result["initial_state"] = initial_state_str + result["states"] = states # List of all states including initial and after each move + + return result + + def _visualize_state(self, pegs_state: Dict[int, List[int]]) -> str: + """ + Create an ASCII visualization of the current state of the pegs. + Adapts to variable number of pegs. + + Args: + pegs_state (dict): Dictionary mapping peg numbers to lists of disks. + + Returns: + str: ASCII art representing the pegs and disks. + """ + # Determine the number of levels based on the maximum number of disks on any peg + max_height = max(len(disks) for disks in pegs_state.values()) + pegs = sorted(pegs_state.keys()) + + visualization = "" + for level in range(max_height, 0, -1): + for peg in pegs: + if len(pegs_state[peg]) >= level: + disk_size = pegs_state[peg][level - 1] + disk_str = f"[{'*' * disk_size}]" + else: + disk_str = "[ ]" + visualization += disk_str.center(7) # Adjust spacing as needed + visualization += "\n" + + # Add the base and peg numbers + visualization += "-" * (7 * len(pegs)) + "\n" + for peg in pegs: + peg_label = f"P{peg}".center(7) + visualization += peg_label + visualization += "\n" + + return visualization + + def _validate_move(self, pegs_state: Dict[int, List[int]], move: str) -> bool: + """ + Validate that a move adheres to the Tower of Hanoi rules. + + Args: + pegs_state (dict): Current state of the pegs. + move (str): Move instruction, e.g., "Move disk 2 from Peg 1 to Peg 3". + + Returns: + bool: True if the move is valid, False otherwise. + """ + try: + parts = move.split() + if len(parts) != 9: + # print(f"Unexpected move format: '{move}'") + return False + disk = int(parts[2]) + from_peg = int(parts[5]) + to_peg = int(parts[8]) + + # Check if the disk to move is the top disk on the from_peg + if not pegs_state[from_peg] or pegs_state[from_peg][-1] != disk: + # print(f"Disk {disk} is not on top of Peg {from_peg}. Current state: {pegs_state[from_peg]}") + return False + + # Check if placing the disk on the to_peg violates size constraints + if pegs_state[to_peg] and pegs_state[to_peg][-1] < disk: + # print(f"Cannot place disk {disk} on top of smaller disk {pegs_state[to_peg][-1]} on Peg {to_peg}.") + return False + + return True + except Exception as e: + print(f"Error validating move '{move}': {e}") + return False + + def _parse_move(self, move: str) -> Tuple[int, int, int]: + """ + Parse a move string and extract disk number, from peg, and to peg. + + Args: + move (str): Move instruction, e.g., "Move disk 2 from Peg 1 to Peg 3". + + Returns: + tuple: (disk, from_peg, to_peg) + """ + pattern = r"Move disk (\d+) from Peg (\d+) to Peg (\d+)" + match = re.match(pattern, move) + if not match: + raise ValueError(f"Unexpected move format: '{move}'") + + disk = int(match.group(1)) + from_peg = int(match.group(2)) + to_peg = int(match.group(3)) + return disk, from_peg, to_peg + + +# Register the dataset +register_dataset("tower_of_hanoi", HanoiDataset, HanoiConfig) diff --git a/reasoning_gym/geometry/__init__.py b/reasoning_gym/geometry/__init__.py new file mode 100644 index 00000000..6e4e2d1a --- /dev/null +++ b/reasoning_gym/geometry/__init__.py @@ -0,0 +1,9 @@ +from .advanced_geometry import AdvancedGeometryConfig, AdvancedGeometryDataset +from .simple_geometry import SimpleGeometryConfig, SimpleGeometryDataset + +__all__ = [ + "SimpleGeometryConfig", + "SimpleGeometryDataset", + "AdvancedGeometryConfig", + "AdvancedGeometryDataset", +] diff --git a/reasoning_gym/geometry/advanced_geometry.py b/reasoning_gym/geometry/advanced_geometry.py new file mode 100644 index 00000000..ac8797b9 --- /dev/null +++ b/reasoning_gym/geometry/advanced_geometry.py @@ -0,0 +1,216 @@ +import random +from dataclasses import dataclass, field +from typing import List, Optional + +import sympy +from sympy.geometry import Point, Segment, Triangle + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class AdvancedGeometryConfig: + """ + Configuration for generating advanced geometry tasks. + """ + + min_coord: int = -10 # Minimum x/y coordinate + max_coord: int = 10 # Maximum x/y coordinate + size: int = 50 # Number of problems to generate + seed: Optional[int] = None + + # Probability or list of tasks we want to generate + # For demonstration, we have three categories: + task_types: List[str] = field( + default_factory=lambda: [ + "orthocenter", + "incircle_radius", + "angle_measure", + ] + ) + + def validate(self): + assert self.min_coord < self.max_coord, "min_coord must be < max_coord." + assert self.size > 0, "Size of dataset must be positive." + assert len(self.task_types) > 0, "Must specify at least one task type." + + +class AdvancedGeometryDataset(ProceduralDataset): + """ + A dataset for advanced geometry tasks using coordinate geometry. + """ + + def __init__(self, config: AdvancedGeometryConfig): + self._prompt_templates = { + "orthocenter": [ + "Given triangle ABC with coordinates A={A}, B={B}, and C={C}, find the coordinates of its orthocenter.", + "For triangle with vertices A={A}, B={B}, and C={C}, determine the orthocenter (intersection of altitudes).", + ], + "incircle_radius": [ + "Consider triangle ABC with coordinates A={A}, B={B}, and C={C}. Compute the radius of its incircle.", + "Find the incircle radius of triangle ABC whose vertices are A={A}, B={B}, and C={C}.", + ], + "angle_measure": [ + "In triangle ABC with coordinates A={A}, B={B}, and C={C}, find the measure (in degrees) of angle ABC.", + "Given a triangle with vertices A={A}, B={B}, C={C}, determine the angle at B in degrees.", + ], + } + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """ + Generate a single advanced geometry item based on the config's task types. + """ + rng = random.Random(self.seed + idx) + task_type = rng.choice(self.config.task_types) + + # Randomly generate coordinates for a triangle + A, B, C = self._generate_non_degenerate_triangle(rng) + + # Build a question and compute the solution + if task_type == "orthocenter": + question, answer, metadata = self._build_orthocenter_task(rng, A, B, C) + elif task_type == "incircle_radius": + question, answer, metadata = self._build_incircle_radius_task(rng, A, B, C) + elif task_type == "angle_measure": + question, answer, metadata = self._build_angle_measure_task(rng, A, B, C) + else: + raise ValueError(f"Unknown task_type: {task_type}") + + return { + "question": question, + "answer": answer, + "metadata": metadata, + } + + def _generate_non_degenerate_triangle(self, rng: random.Random): + """ + Generate a random non-degenerate triangle with integer coordinates + in [min_coord, max_coord] x [min_coord, max_coord]. + """ + max_attempts = 100 + for _ in range(max_attempts): + # Generate points with integer coordinates + points = [] + for _ in range(3): + x = rng.randint(self.config.min_coord, self.config.max_coord) + y = rng.randint(self.config.min_coord, self.config.max_coord) + points.append(Point(x, y)) + + A, B, C = points + + # Calculate signed area to check for non-degeneracy + # Using the formula: 1/2 * |x1(y2 - y3) + x2(y3 - y1) + x3(y1 - y2)| + area = abs(A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y)) / 2 + + if area > 0: + return A, B, C + + raise ValueError(f"Failed to generate a non-degenerate triangle after {max_attempts} attempts.") + + def _build_orthocenter_task(self, rng: random.Random, A: Point, B: Point, C: Point): + """ + Build a question about finding the orthocenter of triangle ABC. + """ + # Convert segments to lines + BC_line = sympy.Line(B, C) + CA_line = sympy.Line(C, A) + + # Calculate altitudes by creating lines perpendicular from each vertex + alt_A = BC_line.perpendicular_line(A) + alt_B = CA_line.perpendicular_line(B) + + # Find orthocenter (intersection of any two altitudes, e.g. alt_A and alt_B) + ortho = alt_A.intersection(alt_B)[0] + + x_ortho_approx = float(ortho.x.evalf()) + y_ortho_approx = float(ortho.y.evalf()) + + question_template = rng.choice(self._prompt_templates["orthocenter"]) + question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y)) + answer_str = f"({x_ortho_approx:.3f}, {y_ortho_approx:.3f})" + + metadata = { + "A": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, C.y), + "orthocenter_exact": (str(ortho.x), str(ortho.y)), + "orthocenter_approx": (x_ortho_approx, y_ortho_approx), + } + return question, answer_str, metadata + + def _build_incircle_radius_task(self, rng: random.Random, A: Point, B: Point, C: Point): + """ + Build a question about finding the incircle radius of triangle ABC. + """ + # Calculate side lengths + a = B.distance(C) + b = C.distance(A) + c = A.distance(B) + + # Semi-perimeter + s = (a + b + c) / 2 + + # Area using Heron's formula + area = sympy.sqrt(s * (s - a) * (s - b) * (s - c)) + + # Radius of incircle = Area / Semi-perimeter + radius = area / s + + # Convert to float for final answer + radius_approx = float(radius.evalf()) + + question_template = rng.choice(self._prompt_templates["incircle_radius"]) + question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y)) + answer_str = f"{radius_approx:.3f}" + + metadata = { + "A": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, C.y), + "incircle_radius_exact": str(radius), + "incircle_radius_approx": radius_approx, + } + return question, answer_str, metadata + + def _build_angle_measure_task(self, rng: random.Random, A: Point, B: Point, C: Point): + """ + Build a question about finding the measure of angle ABC in degrees. + """ + # Angle at B means the angle ∠ABC + # Vector BA = A - B, BC = C - B + BA = A - B + BC = C - B + + # Use vector dot product to find angle between BA and BC + # angle = arccos((BA · BC) / (|BA| * |BC|)) + dot_val = BA.dot(BC) + mag_ba = BA.distance(Point(0, 0)) + mag_bc = BC.distance(Point(0, 0)) + + # numerical check + if mag_ba == 0 or mag_bc == 0: + # degenerate, but theoretically we forced a non-degenerate triangle + angle_deg = 0 + else: + cos_theta = dot_val / (mag_ba * mag_bc) + # clamp cos_theta to [-1, 1] to avoid floating rounding errors + cos_theta = max(-1, min(1, cos_theta)) + angle_rad = sympy.acos(cos_theta) + angle_deg = float(angle_rad.evalf() * 180 / sympy.pi) + + question_template = rng.choice(self._prompt_templates["angle_measure"]) + question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y)) + + answer_str = f"{angle_deg:.2f}°" + metadata = { + "A": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, C.y), + "angle_ABC_degrees": angle_deg, + } + return question, answer_str, metadata + + +# Register the dataset +register_dataset("advanced_geometry", AdvancedGeometryDataset, AdvancedGeometryConfig) diff --git a/reasoning_gym/geometry/simple_geometry.py b/reasoning_gym/geometry/simple_geometry.py new file mode 100644 index 00000000..d04912d7 --- /dev/null +++ b/reasoning_gym/geometry/simple_geometry.py @@ -0,0 +1,140 @@ +import random +from dataclasses import dataclass +from typing import Optional + +from ..factory import ProceduralDataset, register_dataset + + +@dataclass +class SimpleGeometryConfig: + """ + Configuration for generating basic geometry (angle-finding) tasks. + Produces a random convex polygon with N sides, random angles + for the first (N-1) sides, and asks the solver to find the last angle. + """ + + min_sides: int = 3 # Minimum number of sides (e.g. triangle) + max_sides: int = 6 # Maximum number of sides (e.g. hexagon) + min_angle: int = 10 # Minimum angle (in degrees) for each of the first (N-1) angles + max_angle: int = 170 # Maximum angle (in degrees) for each of the first (N-1) angles + seed: Optional[int] = None # Random seed + size: int = 100 # Number of geometry tasks to generate + + def validate(self) -> None: + """ + Validate configuration parameters. + """ + assert self.min_sides >= 3, "min_sides must be at least 3 (triangle)." + assert self.max_sides >= self.min_sides, "max_sides must be >= min_sides." + assert 0 < self.min_angle < 180, "min_angle must be in (0, 180)." + assert self.max_angle <= 179, "max_angle should be less than 180." + assert self.max_angle >= self.min_angle, "max_angle must be >= min_angle." + + +class SimpleGeometryDataset(ProceduralDataset): + """ + A dataset for simple polygon angle-finding tasks. + We randomly choose the number of sides N within [min_sides, max_sides]. + We then generate (N-1) random angles (in degrees), ensuring their sum is + strictly less than the total sum for an (N)-sided convex polygon (which is 180*(N-2)). + The question asks for the missing angle; the answer is computed by subtracting the + sum of known angles from 180*(N-2). + """ + + def __init__(self, config: SimpleGeometryConfig): + self._prompt_templates = [ + ( + "Given a convex polygon with {n_sides} sides, its first {n_minus_1} interior angles " + "are: {angle_list}. What is the measure of the remaining interior angle (in degrees)?" + ), + ( + "A convex polygon has {n_sides} sides. The measures of " + "the first {n_minus_1} interior angles are: {angle_list}. " + "Find the measure of the last interior angle." + ), + ( + "Consider a convex {n_sides}-gon whose first {n_minus_1} interior angles " + "are: {angle_list}. Determine the measure of the remaining angle." + ), + ] + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """ + Generate a single geometry angle-finding item. + + Returns: + A dict with: + - question: str + - answer: str (the missing angle, as an integer or float in degrees) + - metadata: dict (n_sides, angles, sum_of_known, missing_angle, etc.) + """ + rng = random.Random(self.seed + idx) + + # Randomly pick the number of sides + n_sides = rng.randint(self.config.min_sides, self.config.max_sides) + + # Total interior angle sum for a convex n_sides-gon + total_sum = 180 * (n_sides - 2) + + # Generate (n_sides - 1) random angles, ensuring their sum < total_sum + known_angles = self._generate_valid_angles(rng, n_sides, total_sum) + + # Missing angle + missing_angle = total_sum - sum(known_angles) + + # Build the question string + angle_list_str = ", ".join(f"{a:.1f}°" for a in known_angles) + prompt = rng.choice(self._prompt_templates).format( + n_sides=n_sides, n_minus_1=n_sides - 1, angle_list=angle_list_str + ) + + # Round the missing angle to one decimal place or integer if it is very close to an integer + # so that the answer remains consistent and clean + missing_angle_rounded = round(missing_angle, 1) + if abs(missing_angle_rounded - round(missing_angle_rounded)) < 1e-6: + # If it is effectively an integer, keep it as int + missing_angle_rounded = int(missing_angle_rounded) + + answer_str = str(missing_angle_rounded) + + return { + "question": prompt, + "answer": answer_str, + "metadata": { + "n_sides": n_sides, + "known_angles": known_angles, + "sum_of_known_angles": sum(known_angles), + "missing_angle_raw": missing_angle, + "missing_angle_rounded": missing_angle_rounded, + "total_interior_sum": total_sum, + }, + } + + def _generate_valid_angles(self, rng: random.Random, n_sides: int, total_sum: int): + """ + Generate (n_sides - 1) random angles in [min_angle, max_angle], + ensuring the sum is strictly less than total_sum to keep a valid missing angle. + We keep retrying until we find a valid set or reach a max attempt limit. + """ + max_attempts = 100 + for _ in range(max_attempts): + angles = [] + # We choose angles one by one + for _ in range(n_sides - 1): + angle = rng.randint(self.config.min_angle, self.config.max_angle) + angles.append(float(angle)) + + # Check if the sum is strictly less than total_sum + if sum(angles) < total_sum: + return angles + + # If we fail after max_attempts, raise an error + raise ValueError( + f"Could not generate valid angles for an {n_sides}-gon " + f"with total sum {total_sum} within {max_attempts} attempts." + ) + + +# Register the dataset so it can be accessed similarly to the others +register_dataset("simple_geometry", SimpleGeometryDataset, SimpleGeometryConfig) diff --git a/reasoning_gym/graphs/__init__.py b/reasoning_gym/graphs/__init__.py index 89d44687..6bbe7d67 100644 --- a/reasoning_gym/graphs/__init__.py +++ b/reasoning_gym/graphs/__init__.py @@ -2,8 +2,8 @@ from .quantum_lock import QuantumLockConfig, QuantumLockDataset __all__ = [ - "FamilyRelationshipsDataset", "FamilyRelationshipsConfig", + "FamilyRelationshipsDataset", "QuantumLockConfig", "QuantumLockDataset", ] diff --git a/reasoning_gym/graphs/family_relationships.py b/reasoning_gym/graphs/family_relationships.py index 6ba042a8..ee278b33 100644 --- a/reasoning_gym/graphs/family_relationships.py +++ b/reasoning_gym/graphs/family_relationships.py @@ -1,5 +1,5 @@ import random -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import StrEnum from itertools import count from typing import List, Optional, Set, Tuple @@ -37,12 +37,8 @@ class Person: gender: Gender id: int spouse: Optional["Person"] = None - parents: List["Person"] = None - children: List["Person"] = None - - def __post_init__(self): - self.parents = self.parents or [] - self.children = self.children or [] + parents: List["Person"] = field(default_factory=list) + children: List["Person"] = field(default_factory=list) def __hash__(self): return self.id @@ -69,14 +65,8 @@ class FamilyRelationshipsConfig: min_family_size: int = 4 max_family_size: int = 8 - male_names: List[str] = None - female_names: List[str] = None - seed: Optional[int] = None - size: int = 500 - - def __post_init__(self): - # Default name lists if none provided - default_male_names = [ + male_names: List[str] = field( + default_factory=lambda: [ "James", "John", "Robert", @@ -121,7 +111,9 @@ def __post_init__(self): "Ryder", "Finn", ] - default_female_names = [ + ) + female_names: List[str] = field( + default_factory=lambda: [ "Mary", "Patricia", "Jennifer", @@ -166,11 +158,9 @@ def __post_init__(self): "Sky", "Rain", ] - - if self.male_names is None: - self.male_names = default_male_names - if self.female_names is None: - self.female_names = default_female_names + ) + seed: Optional[int] = None + size: int = 500 def validate(self) -> None: """Validate configuration parameters""" diff --git a/reasoning_gym/graphs/quantum_lock.py b/reasoning_gym/graphs/quantum_lock.py index 402b6f0c..5863a5bf 100644 --- a/reasoning_gym/graphs/quantum_lock.py +++ b/reasoning_gym/graphs/quantum_lock.py @@ -28,7 +28,7 @@ def __init__(self, config: QuantumLockConfig): self._prompt_templates = [ """\ In front of you are some buttons, a light, and a number. The light will toggle between red and green whenever you press a button. Each button performs a mathematical operation to the number, but the operation may depend on the state of the light. -You must press the shortest correct sequence of buttons to reach the target value. +You must press the shortest correct sequence of buttons to reach the target value. Your answer should be a sequence of buttons separated by '→', for example: A → B → C Start: {initial_value} ({initial_state}) Target: {target_value} diff --git a/reasoning_gym/logic/__init__.py b/reasoning_gym/logic/__init__.py index c2c07625..38307647 100644 --- a/reasoning_gym/logic/__init__.py +++ b/reasoning_gym/logic/__init__.py @@ -6,10 +6,13 @@ - Syllogisms """ +from .aiw import AliceInWonderlandConfig, AliceInWonderlandDataset from .propositional_logic import PropositionalLogicConfig, PropositionalLogicDataset from .syllogisms import SyllogismConfig, SyllogismDataset, Term __all__ = [ + "AliceInWonderlandConfig", + "AliceInWonderlandDataset", "PropositionalLogicConfig", "PropositionalLogicDataset", "SyllogismConfig", diff --git a/reasoning_gym/logic/aiw.py b/reasoning_gym/logic/aiw.py new file mode 100644 index 00000000..0c864cc4 --- /dev/null +++ b/reasoning_gym/logic/aiw.py @@ -0,0 +1,197 @@ +from dataclasses import dataclass, field +from enum import Enum +from random import Random +from string import Template +from typing import List, Optional + +from ..factory import ProceduralDataset, register_dataset + + +class TaskType(Enum): + """Defines the type of task for the Alice in Wonderland dataset.""" + + SIBLINGS = "siblings" + FRIENDS = "friends" + COLLEAGUES = "colleagues" # Added colleagues task + + +@dataclass +class AliceInWonderlandConfig: + """Configuration options for the Alice in Wonderland dataset. + + Attributes: + male_names (List[str]): List of male names to use in questions. + female_names (List[str]): List of female names to use in questions. Must include 'Alice'. + task_types (List[TaskType]): List of task types to include in dataset. + seed (Optional[int]): Seed for random number generation. + size (int): Number of samples in the dataset. + max_entities (int): Max number of siblings/friends/colleagues in questions. + """ + + male_names: List[str] = field( + default_factory=lambda: [ + "James", + "John", + "Robert", + "Michael", + "William", + "David", + "Richard", + "Joseph", + "Thomas", + "Charles", + "Bob", + ] + ) + female_names: List[str] = field( + default_factory=lambda: [ + "Mary", + "Patricia", + "Jennifer", + "Linda", + "Elizabeth", + "Barbara", + "Susan", + "Jessica", + "Sarah", + "Margaret", + "Alice", + ] + ) + task_types: List[TaskType] = field( + default_factory=lambda: [TaskType.SIBLINGS, TaskType.FRIENDS, TaskType.COLLEAGUES] # Added Colleagues + ) + seed: Optional[int] = None + size: int = 10 + max_entities: int = 6 # Added max_entities + + def validate(self) -> None: + """Validates the configuration parameters.""" + assert len(self.male_names) > 0, "must provide male names" + assert len(self.female_names) > 0, "must provide female names" + assert "Alice" in self.female_names, "'Alice' must be in female names" + assert len(self.task_types) > 0, "must provide at least one task type" + assert self.max_entities > 0, "max_entities must be positive" + + +class AliceInWonderlandDataset(ProceduralDataset): + """ + A procedural dataset inspired by the "Alice in Wonderland" paper. + + The dataset is inspired by the following paper: + @inproceedings{nezhurina2024alice, + title={Alice in Wonderland: Simple Tasks Reveal Severe Generalization and + Basic Reasoning Deficits in State-Of-the-Art Large Language Models}, + author={Marianna Nezhurina and Lucia Cipolina-Kun and Mehdi Cherti and + Jenia Jitsev}, + booktitle={NeurIPS 2024 Workshop on Scientific Methods for Understanding + Deep Learning}, + year={2024}, + url={https://openreview.net/forum?id=Mkl7dzjYiW} + } + + """ + + def __init__(self, config: AliceInWonderlandConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + self.templates = { + TaskType.SIBLINGS: [ + Template( + "$female_name has $num_brothers brothers and she also has " + "$num_sisters sisters. How many sisters does " + "$female_name's brother have?" + ), + Template( + "$female_name has $num_sisters sisters and she also has " + "$num_brothers brothers. How many sisters does " + "$male_name's brother have?" + ), + ], + TaskType.FRIENDS: [ + Template( + "$female_name has $num_male male friends and she also has " + "$num_female female friends. They all are friends with each " + "other and have no other friends aside. How many female " + "friends does $male_name, a male friend of $female_name, " + "have?" + ) + ], + TaskType.COLLEAGUES: [ # New colleagues templates + Template( + "$female_name has $num_male_colleagues_alice_circle male colleagues and she also has " + "$num_female_colleagues_alice_circle female colleagues. These are all colleagues that $female_name has. " + "All these mentioned persons around $female_name are colleagues of each other. " + "$male_name has $num_male_colleagues_bob_circle male colleagues " + "and $num_female_colleagues_bob_circle female colleagues in total. " + "All these mentioned persons around $male_name are colleagues of each other. " + "The people in the circle around $male_name do not have " + "other colleagues aside - with the only exception of Matilda. " + "She is colleague of $male_name and she is also colleague of $female_name, " + "being part of $female_name's circle. How many female colleagues does Matilda have?" + ), + ], + } + + def _get_aiw(self, rng: Random) -> dict: + """Generates a single Alice in Wonderland question. + + Args: + rng (Random): Random number generator. + + Returns: + dict: A dictionary containing the generated question, the right answer + and a description of the example. + """ + task_type = rng.choice(self.config.task_types) + female_name = rng.choice(self.config.female_names) + male_name = rng.choice(self.config.male_names) + + if task_type == TaskType.SIBLINGS: + num_brothers = rng.randint(1, self.config.max_entities) + num_sisters = rng.randint(1, self.config.max_entities) + + answer = num_sisters + 1 + template = rng.choice(self.templates[TaskType.SIBLINGS]) + question = template.substitute( + female_name=female_name, + male_name=male_name, + num_brothers=num_brothers, + num_sisters=num_sisters, + ) + elif task_type == TaskType.FRIENDS: + num_male = rng.randint(1, self.config.max_entities) + num_female = rng.randint(1, self.config.max_entities) + + answer = num_female + 1 + template = rng.choice(self.templates[TaskType.FRIENDS]) + question = template.substitute( + female_name=female_name, + male_name=male_name, + num_male=num_male, + num_female=num_female, + ) + elif task_type == TaskType.COLLEAGUES: + num_male_colleagues_alice_circle = rng.randint(1, self.config.max_entities) + num_female_colleagues_alice_circle = rng.randint(1, self.config.max_entities) + num_male_colleagues_bob_circle = rng.randint(1, self.config.max_entities) + num_female_colleagues_bob_circle = rng.randint(1, self.config.max_entities) + + answer = num_female_colleagues_alice_circle + 1 + template = rng.choice(self.templates[TaskType.COLLEAGUES]) + question = template.substitute( + female_name=female_name, + male_name=male_name, + num_male_colleagues_alice_circle=num_male_colleagues_alice_circle, + num_female_colleagues_alice_circle=num_female_colleagues_alice_circle, + num_male_colleagues_bob_circle=num_male_colleagues_bob_circle, + num_female_colleagues_bob_circle=num_female_colleagues_bob_circle, + ) + + return {"question": question, "answer": answer, "metadata": {"task_type": task_type.value}} + + def __getitem__(self, idx: int) -> dict: + rng = Random(self.seed + idx) + return self._get_aiw(rng) + + +register_dataset("aiw", AliceInWonderlandDataset, AliceInWonderlandConfig) diff --git a/reasoning_gym/logic/syllogisms.py b/reasoning_gym/logic/syllogisms.py index 0af9d1b2..a5bbb219 100644 --- a/reasoning_gym/logic/syllogisms.py +++ b/reasoning_gym/logic/syllogisms.py @@ -206,6 +206,13 @@ def _is_valid_syllogism( return False + def _format_quantifier_statement(self, quantifier: Quantifier, subject: Term, predicate: Term) -> str: + """Format a quantified statement in natural language""" + if quantifier == Quantifier.SOME_NOT: + return f"Some {subject.plural} are not {predicate.plural}" + else: + return f"{quantifier.value} {subject.plural} are {predicate.plural}" + def _generate_syllogism(self, rng: Random) -> dict: """Generate a single syllogism problem""" # Select three different terms @@ -226,9 +233,9 @@ def _generate_syllogism(self, rng: Random) -> dict: conclusion = (rng.choice(quantifiers), terms[0], terms[2]) # Format the syllogism as text - premise1_text = f"{premise1[0].value} {premise1[1].plural} are {premise1[2].plural}" - premise2_text = f"{premise2[0].value} {premise2[1].plural} are {premise2[2].plural}" - conclusion_text = f"{conclusion[0].value} {conclusion[1].plural} are {conclusion[2].plural}" + premise1_text = self._format_quantifier_statement(premise1[0], premise1[1], premise1[2]) + premise2_text = self._format_quantifier_statement(premise2[0], premise2[1], premise2[2]) + conclusion_text = self._format_quantifier_statement(conclusion[0], conclusion[1], conclusion[2]) question = ( f"Consider these statements:\n" diff --git a/requirements-dev.txt b/requirements-dev.txt index 8b1c25a4..b96fc1c2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ pytest>=8.3.4 +pytest-cov>=6.0.0 black>=24.10.0 isort>=5.13.2 flake8>=7.1.1 diff --git a/scripts/generate_gallery.py b/scripts/generate_gallery.py index b9cf4630..08f482e4 100755 --- a/scripts/generate_gallery.py +++ b/scripts/generate_gallery.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 """Generate a markdown gallery of all available datasets with examples""" -import os import textwrap from pathlib import Path -import reasoning_gym.cognition.figlet_fonts -import reasoning_gym.cognition.rubiks_cube +import reasoning_gym.code.bf from reasoning_gym.factory import DATASETS, create_dataset @@ -21,18 +19,17 @@ def generate_gallery() -> str: content.append("## Available Datasets\n") for name in sorted(DATASETS.keys()): # Create anchor link - anchor = name.replace("_", "-").lower() + anchor = name.replace(" ", "-").lower() content.append(f"- [{name}](#{anchor})\n") content.append("\n") # Add examples for each dataset content.append("## Dataset Examples\n") for name in sorted(DATASETS.keys()): - dataset = create_dataset(name) + dataset = create_dataset(name, seed=42) # Add dataset header with anchor - anchor = name.replace("_", "-").lower() - content.append(f"### {name} {{{anchor}}}\n") + content.append(f"### {name}\n") # Get dataset class docstring if available if dataset.__class__.__doc__: @@ -49,7 +46,7 @@ def generate_gallery() -> str: # Show examples content.append("Example tasks:\n") - content.append("```\n") + content.append("````\n") for i, item in enumerate(dataset): if i >= 3: break @@ -59,7 +56,7 @@ def generate_gallery() -> str: if item.get("metadata"): content.append(f"Metadata: {item['metadata']}\n") content.append("\n") - content.append("```\n\n") + content.append("````\n\n") return "".join(content) @@ -76,6 +73,7 @@ def main(): with open(gallery_path, "w") as f: f.write(gallery_content) + f.write("\n") print(f"Generated gallery at {gallery_path}") diff --git a/tests/test_advanced_geometry.py b/tests/test_advanced_geometry.py new file mode 100644 index 00000000..9eec1b36 --- /dev/null +++ b/tests/test_advanced_geometry.py @@ -0,0 +1,83 @@ +import pytest + +from reasoning_gym.geometry.advanced_geometry import AdvancedGeometryConfig, AdvancedGeometryDataset + + +def test_advanced_geometry_config_validation(): + """Test that invalid configs raise appropriate errors.""" + # min_coord >= max_coord + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(min_coord=5, max_coord=5) + config.validate() + + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(min_coord=10, max_coord=0) + config.validate() + + # size <= 0 + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(size=0) + config.validate() + + # Empty task_types + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(task_types=[]) + config.validate() + + +def test_advanced_geometry_dataset_deterministic(): + """Test the dataset generates the same items with the same seed.""" + config = AdvancedGeometryConfig(min_coord=-5, max_coord=5, size=5, seed=42) + dataset1 = AdvancedGeometryDataset(config) + dataset2 = AdvancedGeometryDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i], ( + f"Item mismatch at index {i} for same seed. " f"Dataset1: {dataset1[i]} vs Dataset2: {dataset2[i]}" + ) + + +def test_advanced_geometry_dataset_items(): + """Test basic properties of generated items.""" + config = AdvancedGeometryConfig(min_coord=-3, max_coord=3, size=5, seed=123) + dataset = AdvancedGeometryDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check structure + assert isinstance(item, dict), "Generated item must be a dictionary." + assert "question" in item, "Item must contain a 'question' key." + assert "answer" in item, "Item must contain an 'answer' key." + assert "metadata" in item, "Item must contain a 'metadata' key." + + # Basic metadata checks + metadata = item["metadata"] + assert ( + "A" in metadata and "B" in metadata and "C" in metadata + ), "Metadata should contain coordinates for points A, B, and C." + + # Check answer format depending on task type + # For angle measure tasks, answer should end with '°' + if "angle_measure" in item["question"].lower() or "angle at" in item["question"].lower(): + assert item["answer"].endswith("°"), f"Expected angle measure in degrees, got {item['answer']}" + + +def test_advanced_geometry_dataset_iteration(): + """Test that iteration respects dataset size and is repeatable.""" + config = AdvancedGeometryConfig(min_coord=-2, max_coord=2, size=3, seed=999) + dataset = AdvancedGeometryDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size, "Iterator should yield exactly 'size' items." + + # Test list conversion + items_list = list(dataset) + assert len(items_list) == config.size, "List conversion should yield exactly 'size' items." + + # Test multiple iterations produce the same results + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items, "Multiple iterations should yield the same items." diff --git a/tests/test_aiw.py b/tests/test_aiw.py new file mode 100644 index 00000000..5a2fb454 --- /dev/null +++ b/tests/test_aiw.py @@ -0,0 +1,96 @@ +import pytest + +from reasoning_gym.logic.aiw import AliceInWonderlandConfig, AliceInWonderlandDataset, TaskType + + +def test_aiw_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = AliceInWonderlandConfig(male_names=[]) # Empty male names + config.validate() + + with pytest.raises(AssertionError): + config = AliceInWonderlandConfig(female_names=[]) # Empty female names + config.validate() + + with pytest.raises(AssertionError): + config = AliceInWonderlandConfig(female_names=["Mary", "Jane"]) # No Alice + config.validate() + + with pytest.raises(AssertionError): + config = AliceInWonderlandConfig(task_types=[]) # No task types + config.validate() + + +def test_aiw_deterministic(): + """Test that dataset generates same items with same seed""" + config = AliceInWonderlandConfig(seed=42, size=10) + dataset1 = AliceInWonderlandDataset(config) + dataset2 = AliceInWonderlandDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_aiw_items(): + """Test basic properties of generated items""" + config = AliceInWonderlandConfig(size=50, seed=42) + dataset = AliceInWonderlandDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Verify answer is numeric and positive + answer = int(item["answer"]) + assert answer > 0 + + # Verify question contains at least one female name + female_names = config.female_names + assert any(name in item["question"] for name in female_names) + + # Verify question task type characteristics + task_type = item["metadata"]["task_type"] + if task_type == TaskType.SIBLINGS.value: + assert any(phrase in item["question"] for phrase in ["brothers", "sisters"]) + elif task_type == TaskType.FRIENDS.value: + assert "friends" in item["question"] + elif task_type == TaskType.COLLEAGUES: + assert "colleagues" in item["question"] + + +def test_aiw_iteration(): + """Test that iteration works correctly""" + config = AliceInWonderlandConfig(size=5, seed=42) + dataset = AliceInWonderlandDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size + + # Test list conversion + items = list(dataset) + assert len(items) == config.size + + # Test multiple iterations yield same results + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items + + +def test_aiw_random_ranges(): + """Test that generated numbers stay within expected ranges""" + config = AliceInWonderlandConfig(size=30, seed=42, max_entities=12) + dataset = AliceInWonderlandDataset(config) + + for item in dataset: + question = item["question"] + numbers = [int(n) for n in question.split() if n.isdigit()] + + # Check all numbers are in reasonable range (1-6 as per implementation) + assert all(1 <= n <= 12 for n in numbers), f"Numbers out of range: {numbers}" diff --git a/tests/test_arc_1d.py b/tests/test_arc_1d.py new file mode 100644 index 00000000..1679fb50 --- /dev/null +++ b/tests/test_arc_1d.py @@ -0,0 +1,107 @@ +import pytest + +from reasoning_gym.cognition import Arc1DConfig, Arc1DDataset + + +def test_arc_1d_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = Arc1DConfig(min_size=0) + config.validate() + + with pytest.raises(AssertionError): + config = Arc1DConfig(min_size=30, max_size=20) + config.validate() + + with pytest.raises(AssertionError): + config = Arc1DConfig(num_train=0) + config.validate() + + +def test_arc_1d_deterministic(): + """Test that dataset generates same items with same seed""" + config = Arc1DConfig(seed=42, size=10) + dataset1 = Arc1DDataset(config) + dataset2 = Arc1DDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_arc_1d_items(): + """Test basic properties of generated items""" + config = Arc1DConfig(min_size=10, max_size=15, num_train=2, size=50, seed=42) + dataset = Arc1DDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Check metadata contents + metadata = item["metadata"] + assert "task_name" in metadata + assert "size" in metadata + assert "train_examples" in metadata + assert "test_example" in metadata + + # Verify size constraints + assert config.min_size <= metadata["size"] <= config.max_size + + # Check training examples + train_examples = metadata["train_examples"] + assert len(train_examples) == config.num_train + for example in train_examples: + assert "input" in example + assert "output" in example + assert len(example["input"]) == metadata["size"] + assert len(example["output"]) == metadata["size"] + + # Check test example + test_example = metadata["test_example"] + assert "input" in test_example + assert "output" in test_example + assert len(test_example["input"]) == metadata["size"] + assert len(test_example["output"]) == metadata["size"] + + +def test_arc_1d_iteration(): + """Test that iteration respects dataset size""" + config = Arc1DConfig(size=5, seed=42) # Small size for testing + dataset = Arc1DDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size, "Iterator should yield exactly size items" + + # Test list conversion + items = list(dataset) + assert len(items) == config.size, "Iterator should yield exactly size items" + + # Test multiple iterations + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items, "Multiple iterations should yield same items" + + +def test_arc_1d_scoring(): + """Test answer scoring logic""" + config = Arc1DConfig(size=1, seed=42) + dataset = Arc1DDataset(config) + entry = dataset[0] + + # Test exact match + assert dataset.score_answer(entry["answer"], entry) == 1.0 + + # Test partial match (answer contained within response) + assert dataset.score_answer(f"The answer is: {entry['answer']}", entry) == 0.5 + + # Test incorrect answer + assert dataset.score_answer("wrong answer", entry) == 0.01 + + # Test None answer + assert dataset.score_answer(None, entry) == 0.0 diff --git a/tests/test_arc_1d_tasks.py b/tests/test_arc_1d_tasks.py new file mode 100644 index 00000000..98d9d2ef --- /dev/null +++ b/tests/test_arc_1d_tasks.py @@ -0,0 +1,122 @@ +import random + +import pytest + +from reasoning_gym.cognition.arc_1d_tasks import ( + task_block_and_noise_remove, + task_block_and_noise_remove_inside, + task_block_scale_to_dot, + task_block_touch_dot, + task_block_touch_dot_n_pix, + task_change_to_five, + task_color_left_half_blocks, + task_copy_block_to_dots, + task_copy_block_to_dots_colors, + task_duplicate_block_from_seeds, + task_fill_from_pixel, + task_fill_until_collision, + task_gravity, + task_gravity_antigravity, + task_gravity_counting, + task_gravity_one_step, + task_gravity_weighted_colors, + task_identity, + task_inverse, + task_mark_size_two_blocks, + task_mirror, + task_move_block_by_own_size, + task_move_n_pix, + task_move_n_pix_wrapped, + task_paint_biggest_block, + task_recolor_blocks_by_size, + task_recolor_blocks_from_palette, + task_reflect_block_around_dot, + task_reflect_block_with_border_pixel, + task_reflect_block_with_border_pixel_random, + task_repeat_pattern_full, + task_sort_blocks_by_size, + task_sort_complete_sequence, + task_two_points_and_fill, +) + + +def test_all_arc_1d_tasks(): + """Test that all ARC 1D task functions can be executed without exceptions.""" + rng = random.Random(42) # Fixed seed for reproducibility + size = 20 # Reasonable size for testing + + # Test all task functions + # Fixed move_pix value for testing + move_pix = 2 + + # Test task augmentation functions + base_task = task_move_n_pix(rng, size, move_pix, True) + assert base_task is not None + + mirrored = task_mirror(base_task) + assert mirrored is not None + assert mirrored["input"] == list(reversed(base_task["input"])) + assert mirrored["output"] == list(reversed(base_task["output"])) + + inversed = task_inverse(base_task) + assert inversed is not None + assert inversed["input"] == base_task["output"] + assert inversed["output"] == base_task["input"] + + identical = task_identity(base_task) + assert identical is not None + assert identical == base_task + + tasks = [ + (task_move_n_pix, {"move_pix": move_pix, "solid": True}), + (task_move_n_pix_wrapped, {"move_pix": move_pix, "solid": True}), + (task_gravity, {}), + (task_gravity_counting, {}), + (task_gravity_antigravity, {}), + (task_block_touch_dot, {}), + (task_block_touch_dot_n_pix, {"move_pix": move_pix}), + (task_block_scale_to_dot, {}), + (task_two_points_and_fill, {}), + (task_reflect_block_with_border_pixel, {}), + (task_reflect_block_with_border_pixel_random, {}), + (task_reflect_block_around_dot, {}), + (task_block_and_noise_remove, {}), + (task_block_and_noise_remove_inside, {}), + (task_copy_block_to_dots, {}), + (task_copy_block_to_dots_colors, {}), + (task_paint_biggest_block, {}), + (task_sort_blocks_by_size, {}), + (task_sort_complete_sequence, {}), + (task_recolor_blocks_by_size, {}), + (task_gravity_one_step, {}), + (task_move_block_by_own_size, {}), + (task_change_to_five, {}), + (task_recolor_blocks_from_palette, {}), + (task_duplicate_block_from_seeds, {}), + (task_fill_from_pixel, {}), + (task_mark_size_two_blocks, {}), + (task_fill_until_collision, {}), + (task_repeat_pattern_full, {}), + (task_gravity_weighted_colors, {}), + (task_color_left_half_blocks, {}), + ] + + for task_func, kwargs in tasks: + # Try multiple times as some functions might return None for certain inputs + success = False + for _ in range(10): # Try up to 10 times + try: + result = task_func(rng, size, **kwargs) + if result is not None: + success = True + # Basic structure checks + assert isinstance(result, dict) + assert "input" in result + assert "output" in result + assert len(result["input"]) == size + assert len(result["output"]) == size + break + except Exception as e: + pytest.fail(f"Task {task_func.__name__} failed with error: {str(e)}") + + assert success, f"Task {task_func.__name__} always returned None in 10 attempts" diff --git a/tests/test_base_conversion.py b/tests/test_base_conversion.py index 7c8edf1e..8017d74a 100644 --- a/tests/test_base_conversion.py +++ b/tests/test_base_conversion.py @@ -65,9 +65,20 @@ def test_base_conversion_dataset_items(): # Verify conversion correctness decimal_value = item["metadata"]["decimal_value"] target_base = item["metadata"]["target_base"] - expected = format(decimal_value, "x" if target_base == 16 else "b" if target_base == 2 else "").strip() - if target_base not in (2, 16): - expected = format(decimal_value, f"{target_base}x").lower().strip() + + # Use same conversion logic as implementation + if target_base == 16: + expected = format(decimal_value, "x") + elif target_base == 2: + expected = format(decimal_value, "b") + else: + # Manual conversion for other bases + n = decimal_value + digits = [] + while n: + digits.append(int(n % target_base)) + n //= target_base + expected = "".join(str(d) if d < 10 else chr(ord("a") + d - 10) for d in reversed(digits) or [0]) assert item["answer"] == expected @@ -83,6 +94,25 @@ def test_base_conversion_dataset_iteration(): assert items == list(dataset) +def test_base_conversion_validity(): + """Test that generated numbers are valid for their bases""" + config = BaseConversionConfig(min_base=2, max_base=36, min_value=0, max_value=1000, size=100, seed=42) + dataset = BaseConversionDataset(config) + + def is_valid_for_base(num_str: str, base: int) -> bool: + valid_chars = "0123456789abcdefghijklmnopqrstuvwxyz"[:base] + return all(c in valid_chars for c in num_str.lower()) + + for i in range(len(dataset)): + item = dataset[i] + assert is_valid_for_base( + item["metadata"]["source_repr"], item["metadata"]["source_base"] + ), f"Invalid source number {item['metadata']['source_repr']} for base {item['metadata']['source_base']}" + assert is_valid_for_base( + item["metadata"]["target_repr"], item["metadata"]["target_base"] + ), f"Invalid target number {item['metadata']['target_repr']} for base {item['metadata']['target_base']}" + + def test_base_conversion_special_bases(): """Test conversion between special bases (binary, hex)""" config = BaseConversionConfig( diff --git a/tests/test_arithmetic.py b/tests/test_basic_arithmetic.py similarity index 100% rename from tests/test_arithmetic.py rename to tests/test_basic_arithmetic.py diff --git a/tests/test_bf.py b/tests/test_bf.py new file mode 100644 index 00000000..86d2619a --- /dev/null +++ b/tests/test_bf.py @@ -0,0 +1,38 @@ +import pytest + +from reasoning_gym.code.bf import BFConfig, BFDataset + + +def test_bf(): + """Test basic properties and solution of generated items""" + + # Easy + config = BFConfig(seed=42, size=20, difficulty=1) + dataset = BFDataset(config) + + for item in dataset: + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Check metadata contains required fields + assert "bfit_code" in item["metadata"] + assert "bf_program" in item["metadata"] + + # Test the scoring + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 + assert dataset.score_answer(answer=None, entry=item) == 0.0 + assert dataset.score_answer(answer="Love is a battlefield", entry=item) == 0.01 + + # Medium + config = BFConfig(seed=43, size=20, difficulty=2) + dataset = BFDataset(config) + for item in dataset: + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 + + # Hard + config = BFConfig(seed=44, size=20, difficulty=3) + dataset = BFDataset(config) + for item in dataset: + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 diff --git a/tests/test_calendar_arithmetic.py b/tests/test_calendar_arithmetic.py new file mode 100644 index 00000000..87f84781 --- /dev/null +++ b/tests/test_calendar_arithmetic.py @@ -0,0 +1,198 @@ +import calendar +import math +from datetime import date + +import pytest + +from reasoning_gym.arithmetic import CalendarArithmeticConfig, CalendarArithmeticDataset + +WEEKDAYS = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +] + +WEEKDAY_TASKS = { + "weekday_offset", + "weekday_of_date_from_first_day", + "weekday_of_date", +} +NUMERIC_TASKS = { + "count_days", + "count_business_days", +} +DAY_TASKS = {"recurring_event_day"} +BOOLEAN_TASKS = {"is_leap_year"} +CALENDAR_TASKS = WEEKDAY_TASKS | NUMERIC_TASKS | DAY_TASKS | BOOLEAN_TASKS + + +def test_calendar_config_validation(): + """Test that invalid CalendarArithmeticConfig parameters raise appropriate errors.""" + with pytest.raises(ValueError): + config = CalendarArithmeticConfig(year=0) + config.validate() + + with pytest.raises(ValueError): + config = CalendarArithmeticConfig(size=0) + config.validate() + + with pytest.raises(ValueError): + config = CalendarArithmeticConfig(seed="not_an_int") + config.validate() + + with pytest.raises(ValueError): + config = CalendarArithmeticConfig(tasks=["invalid_task"]) + + +def test_calendar_deterministic(): + """Test that a dataset with a fixed seed produces the same items.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=10) + ds1 = CalendarArithmeticDataset(config) + ds2 = CalendarArithmeticDataset(config) + + for i in range(len(ds1)): + assert ds1[i] == ds2[i] + + +def test_calendar_item_structure(): + """Test that dataset items have the correct structure and fields.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=50) + dataset = CalendarArithmeticDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert all(key in item for key in ["question", "answer", "metadata"]) + + assert isinstance(item["question"], str) and len(item["question"]) > 0 + assert isinstance(item["answer"], str) and len(item["answer"]) > 0 + assert "task" in item["metadata"] + assert item["metadata"]["task"] in CALENDAR_TASKS + + +def test_calendar_answer_format(): + """Test that answers have the correct format based on task type.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=100) + dataset = CalendarArithmeticDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + task = item["metadata"]["task"] + answer = item["answer"] + + if task in WEEKDAY_TASKS: + assert answer in WEEKDAYS + + elif task in NUMERIC_TASKS: + try: + num = int(answer) + assert num >= 0, f"task {task} produced a negative count: {num}" + except ValueError: + pytest.fail(f"task {task} produced a non-integer answer: {answer}") + + elif task in BOOLEAN_TASKS: + assert answer in ["Yes", "No"] + + elif task in DAY_TASKS: + try: + num = int(answer) + year = item["metadata"]["year"] + month = item["metadata"]["month"] + _, last_day = calendar.monthrange(year, month) + assert 1 <= num <= last_day + except ValueError: + pytest.fail(f"task {task} produced a day outside expected range (1-{last_day}): {answer}") + + +def test_scoring_function(): + """Test scoring function for different answer types.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=1) + dataset = CalendarArithmeticDataset(config) + + weekday_item = {"answer": "Monday", "metadata": {"task": "weekday_offset"}} + + assert dataset.score_answer("Monday", weekday_item) == 1.0 + assert dataset.score_answer("Tuesday", weekday_item) == 0.1 + assert dataset.score_answer("It is Monday", weekday_item) == 0.0 + assert dataset.score_answer("no weekday here", weekday_item) == 0.0 + assert dataset.score_answer(None, weekday_item) == 0.0 + + numeric_item = {"answer": "10", "metadata": {"task": "count_business_days"}} + assert dataset.score_answer("10", numeric_item) == 1.0 + assert dataset.score_answer("15", numeric_item) == pytest.approx(math.exp(-5 * 0.5)) + assert dataset.score_answer("no number", numeric_item) == 0.0 + assert dataset.score_answer(None, numeric_item) == 0.0 + + boolean_item = {"answer": "Yes", "metadata": {"task": "is_leap_year"}} + assert dataset.score_answer("Yes", boolean_item) == 1.0 + assert dataset.score_answer("yes", boolean_item) == 1.0 + assert dataset.score_answer("nyes", boolean_item) == 0.0 + assert dataset.score_answer(None, boolean_item) == 0.0 + + +def test_calendar_date_consistency(): + """Test that dates in metadata are consistent with config year.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=50) + dataset = CalendarArithmeticDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + task = item["metadata"]["task"] + + if task == "weekday_offset": + start_date = date.fromisoformat(item["metadata"]["start_date"]) + assert start_date.year == config.year + + elif task in {"weekday_of_date_from_first_day", "weekday_of_date"}: + target_date = date.fromisoformat(item["metadata"]["target_date"]) + assert target_date.year == config.year + + elif task in {"count_business_days", "count_days"}: + start_date = date.fromisoformat(item["metadata"]["start_date"]) + end_date = date.fromisoformat(item["metadata"]["end_date"]) + assert start_date.year == config.year + assert end_date.year == config.year + + elif task == "recurring_event_day": + meta_year = item["metadata"]["year"] + month = item["metadata"]["month"] + answer = int(item["answer"]) + assert meta_year == config.year + assert 1 <= month <= 12 + if answer != -1: + _, last_day = calendar.monthrange(meta_year, month) + assert 1 <= answer <= last_day + + elif task == "is_leap_year": + year = item["metadata"]["year"] + assert config.year - 200 <= year <= config.year + 200 + is_leap_metadata = item["metadata"]["is_leap"] + computed_is_leap = calendar.isleap(year) + assert is_leap_metadata == computed_is_leap + + +def test_calendar_iteration(): + """Test that dataset iteration works correctly and is deterministic.""" + config = CalendarArithmeticConfig(year=2024, seed=42, size=5) + dataset = CalendarArithmeticDataset(config) + + items = [item for item in dataset] + assert len(items) == config.size + + first_iter = list(dataset) + second_iter = list(dataset) + assert first_iter == second_iter + + +def test_task_case_sensitivity(): + """Test that task names are case-insensitive.""" + tasks = ["WEEKDAY_OFFSET", "Count_Business_Days"] + config = CalendarArithmeticConfig(tasks=tasks, size=10) + dataset = CalendarArithmeticDataset(config) + + for item in dataset: + assert item["metadata"]["task"] in [t.lower() for t in tasks] diff --git a/tests/test_countdown.py b/tests/test_countdown.py index e426caf2..e78a69ab 100644 --- a/tests/test_countdown.py +++ b/tests/test_countdown.py @@ -64,6 +64,16 @@ def test_countdown_game_items(): # Verify expression evaluates correctly expr = item["metadata"]["expression"] + + # check score + assert dataset.score_answer(answer=expr, metadata=item["metadata"]) == 1.0 # correct answer + assert dataset.score_answer(answer="45+2", metadata=item["metadata"]) == 0.05 # wrong answer but an attempt + assert ( + dataset.score_answer(answer="a wrong solution", metadata=item["metadata"]) == 0.01 + ) # wrong answer but incorrectly formatted + assert dataset.score_answer(answer="", metadata=item["metadata"]) == 0.01 # wrong answer but empty string + assert dataset.score_answer(answer=None, metadata=item["metadata"]) == 0.0 # no answer + try: result = eval(expr) # Safe here since we control expression generation assert result == item["metadata"]["target"] diff --git a/tests/test_game_of_life.py b/tests/test_game_of_life.py new file mode 100644 index 00000000..df0f133d --- /dev/null +++ b/tests/test_game_of_life.py @@ -0,0 +1,27 @@ +import pytest + +from reasoning_gym.games.game_of_life import GameOfLifeConfig, GameOfLifeDataset + + +def test_game_of_life(): + """Test basic properties and solution of generated items""" + + # Easy + config = GameOfLifeConfig(seed=42, size=1, grid_size_x=20, grid_size_y=20, filled_cells=10, simulation_steps=1) + dataset = GameOfLifeDataset(config) + + for item in dataset: + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # # Check metadata contains required fields + assert "grid_size_x" in item["metadata"] + assert "grid_size_y" in item["metadata"] + assert "filled_cells" in item["metadata"] + assert "simulation_steps" in item["metadata"] + + # # Test the scoring + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 + assert dataset.score_answer(answer=None, entry=item) == 0.0 diff --git a/tests/test_intermediate_integration.py b/tests/test_intermediate_integration.py new file mode 100644 index 00000000..df62ea76 --- /dev/null +++ b/tests/test_intermediate_integration.py @@ -0,0 +1,144 @@ +"""Tests for intermediate integration task generation""" + +import pytest +import sympy +from sympy.parsing.sympy_parser import parse_expr + +from reasoning_gym.algebra.intermediate_integration import IntermediateIntegrationConfig, IntermediateIntegrationDataset + + +def test_intermediate_integration_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(problem_types=["invalid_problem_type"]) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(substitution_types=["invalid_substitution_type"]) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(by_parts_types=["invalid_by_parts_type"]) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(linear_lower_bound=2, linear_upper_bound=1) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(linear_lower_bound=0) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(min_linear_degree=5, max_linear_degree=1) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(min_linear_degree=0) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(outer_constant_min=5, outer_constant_max=1) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(outer_constant_min=0) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(min_poly_degree=5, max_poly_degree=1) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(min_poly_degree=0) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(symbols=("x", "y")) + config.validate() + + with pytest.raises(AssertionError): + config = IntermediateIntegrationConfig(operators=("+", "-", "*", "/")) + config.validate() + + +def test_intermediate_integration_dataset_deterministic(): + """Test that dataset generates same items with same seed""" + config = IntermediateIntegrationConfig(seed=42, size=10) + dataset1 = IntermediateIntegrationDataset(config) + dataset2 = IntermediateIntegrationDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_intermediate_integration_dataset_items(): + """Test that dataset items are valid""" + config = IntermediateIntegrationConfig(seed=42, size=10) + dataset = IntermediateIntegrationDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + assert "integrand" in item["metadata"] + assert "problem_type" in item["metadata"] + assert "variable" in item["metadata"] + assert "type" in item["metadata"] + + # verify answer is mathematical expression + answer = item["answer"] + answer = answer.replace(" + C", "") + assert isinstance(parse_expr(answer), sympy.Expr) + + +def test_verify_answer(): + config = IntermediateIntegrationConfig(seed=42) + dataset = IntermediateIntegrationDataset(config) + for i in range(len(dataset)): + item = dataset[i] + score = dataset.score_answer(item["answer"], item["metadata"]) + assert score == 1.0 + + +def test_score_answer_cases(): + """Test various answer scoring scenarios""" + config = IntermediateIntegrationConfig(seed=42) + dataset = IntermediateIntegrationDataset(config) + x = sympy.Symbol("x") + X = sympy.Symbol("X") + + # Test cases: (answer, metadata, expected_score) + test_cases = [ + # Correct answers + ("x**2 + C", {"variable": "x", "integrand": "2*x"}, 1.0), + ("X**3 - 5*X + C", {"variable": "X", "integrand": "3*X**2 - 5"}, 1.0), + ("sin(x) + C", {"variable": "x", "integrand": "cos(x)"}, 1.0), + # Correct without explicit constant + ("x**2", {"variable": "x", "integrand": "2*x"}, 1.0), + ("log(x)", {"variable": "x", "integrand": "1/x"}, 1.0), + # Incorrect but properly formatted + ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.05), + # Malformed expressions + ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.01), + ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.01), + # Empty answer + ("", {"variable": "x", "integrand": "2*x"}, 0.01), + # Case sensitivity + ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.05), + ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + # Alternative constant notation + ("x**2 + K", {"variable": "x", "integrand": "2*x"}, 1.0), + ("sin(x) + D", {"variable": "x", "integrand": "cos(x)"}, 1.0), + # Simplification required + ("x**2 + C + 5 - 5", {"variable": "x", "integrand": "2*x"}, 1.0), + ("(x**3)/3 - 2*x + C", {"variable": "x", "integrand": "x**2 - 2"}, 1.0), + ] + + for answer, metadata, expected in test_cases: + score = dataset.score_answer(answer, metadata) + assert score == expected, f"Failed case: {answer} | Expected {expected}, got {score}" diff --git a/tests/test_n_queens.py b/tests/test_n_queens.py new file mode 100644 index 00000000..16911220 --- /dev/null +++ b/tests/test_n_queens.py @@ -0,0 +1,143 @@ +"""Tests for N Queens puzzle generation""" + +import pytest + +from reasoning_gym.games.n_queens import NQueensConfig, NQueensDataset + + +def test_nqueens_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = NQueensConfig(n=-1) # Negative not allowed + config.validate() + + with pytest.raises(AssertionError): + config = NQueensConfig(n=0) # Zero not allowed + config.validate() + + with pytest.raises(AssertionError): + config = NQueensConfig(n=5, min_remove=5, max_remove=4) # max < min + config.validate() + + with pytest.raises(AssertionError): + config = NQueensConfig(n=5, min_remove=3, max_remove=6) # n < max + config.validate() + + +def test_nqueens_dataset_deterministic(): + """Test that dataset generates same items with same seed""" + config = NQueensConfig(seed=42, size=10) + dataset1 = NQueensDataset(config) + dataset2 = NQueensDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_nqueens_dataset_items(): + """Test basic properties of generated items""" + config = NQueensConfig(n=8, min_remove=1, max_remove=7, size=10, seed=42) + dataset = NQueensDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check item structure + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Check metadata + assert "puzzle" in item["metadata"] + assert "solutions" in item["metadata"] + assert "num_removed" in item["metadata"] + + puzzle = item["metadata"]["puzzle"] + solutions = item["metadata"]["solutions"] + num_removed = item["metadata"]["num_removed"] + + # Verify board dimensions + assert len(puzzle) == 8 + assert all(len(row) == 8 for row in puzzle) + for board in solutions: + assert len(board) == 8 + assert all(len(row) == 8 for row in board) + + # Verify empty cell count + removed_count = len(puzzle) - sum(1 for row in puzzle for cell in row if cell == "Q") + assert config.min_remove <= removed_count <= config.max_remove + assert removed_count == num_removed + + # Verify solution validity + for board in solutions: + assert is_valid_solution(board) + + # Verify puzzle matches solution where filled + for i in range(8): + for j in range(8): + if puzzle[i][j] == "Q": + assert puzzle[i][j] == board[i][j] + + +def test_nqueens_dataset_iteration(): + """Test that iteration respects dataset size""" + config = NQueensConfig(size=5, seed=42) + dataset = NQueensDataset(config) + + items = list(dataset) + assert len(items) == config.size + + # Test multiple iterations yield same items + assert items == list(dataset) + + +def test_nqueens_board_generation(): + """Test that generated boards are valid""" + config = NQueensConfig(n=10, size=5, seed=42) + dataset = NQueensDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + for board in item["metadata"]["solutions"]: + assert is_valid_solution(board) + + +def test_nqueens_score_answer(): + """Test the score_answer method""" + config = NQueensConfig(n=8, size=10, seed=42) + dataset = NQueensDataset(config) + + # Test a few items + for i in range(len(dataset)): + item = dataset[i] + + # Test correct answer gets score 1.0 + valid_answer = item["metadata"]["valid_answers"][0] + assert dataset.score_answer(valid_answer, item) == 1.0 + + # Test invalid answer gets score 0.01 + invalid_answer = "_ _ _ _\n_ _ _ _\n_ _ _ _\n_ _ _ _" + assert dataset.score_answer(invalid_answer, item) == 0.01 + + # Test None answer gets score 0.0 + assert dataset.score_answer(None, item) == 0.0 + + +def is_valid_solution(board: list[list[str]]) -> bool: + """Helper function to verify N Queens solution validity""" + rows, cols, diags, off_diags = set(), set(), set(), set() + n = len(board) + num_queens = 0 + + for r in range(n): + for c in range(n): + if board[r][c] == "Q": + num_queens += 1 + if r in rows or c in cols or (r + c) in diags or (r - c) in off_diags: + return False + rows.add(r) + cols.add(c) + diags.add(r + c) + off_diags.add(r - c) + + return num_queens == n diff --git a/tests/test_simple_geometry.py b/tests/test_simple_geometry.py new file mode 100644 index 00000000..804cf15a --- /dev/null +++ b/tests/test_simple_geometry.py @@ -0,0 +1,80 @@ +import pytest + +from reasoning_gym.geometry.simple_geometry import SimpleGeometryConfig, SimpleGeometryDataset + + +def test_simple_geometry_config_validation(): + """Test invalid configs raise appropriate errors.""" + # min_sides < 3 + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_sides=2, max_sides=5) + config.validate() + + # max_sides < min_sides + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_sides=4, max_sides=3) + config.validate() + + # Invalid angles + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_angle=-10) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_angle=10, max_angle=5) + config.validate() + + +def test_simple_geometry_dataset_deterministic(): + """Test the dataset generates the same items with the same seed.""" + config = SimpleGeometryConfig(seed=42, size=5, min_sides=3, max_sides=4) + dataset1 = SimpleGeometryDataset(config) + dataset2 = SimpleGeometryDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i], ( + f"Item mismatch at index {i} for same seed. " f"Dataset1: {dataset1[i]} vs Dataset2: {dataset2[i]}" + ) + + +def test_simple_geometry_dataset_items(): + """Test basic properties of generated items.""" + config = SimpleGeometryConfig(min_sides=3, max_sides=5, min_angle=10, max_angle=120, size=10, seed=123) + dataset = SimpleGeometryDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check structure + assert isinstance(item, dict), "Generated item must be a dictionary." + assert "question" in item, "Item must contain a 'question' key." + assert "answer" in item, "Item must contain an 'answer' key." + assert "metadata" in item, "Item must contain a 'metadata' key." + + metadata = item["metadata"] + assert "n_sides" in metadata, "Metadata should contain 'n_sides'." + assert "missing_angle_rounded" in metadata, "Metadata should contain the computed 'missing_angle_rounded'." + + # Check that the missing angle is a valid float or integer + missing_angle = float(item["answer"]) + assert missing_angle > 0, f"Missing angle should be positive, found {missing_angle}" + + +def test_simple_geometry_dataset_iteration(): + """Test that iteration respects dataset size and is repeatable.""" + config = SimpleGeometryConfig(min_sides=3, max_sides=4, size=5, seed=42) + dataset = SimpleGeometryDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size, "Iterator should yield exactly 'size' items." + + # Test list conversion + items_list = list(dataset) + assert len(items_list) == config.size, "List conversion should yield exactly 'size' items." + + # Test multiple iterations produce the same results + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items, "Multiple iterations should yield the same items." diff --git a/tests/test_simple_integration.py b/tests/test_simple_integration.py new file mode 100644 index 00000000..0de8ab36 --- /dev/null +++ b/tests/test_simple_integration.py @@ -0,0 +1,117 @@ +import pytest +import sympy +from sympy.parsing.sympy_parser import parse_expr + +from reasoning_gym.algebra.simple_integration import SimpleIntegrationConfig, SimpleIntegrationDataset + + +def test_simple_integration_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(min_bounds=0) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(max_bounds=5, min_bounds=10) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(min_terms=-1) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(max_terms=2, min_terms=5) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(min_degree=-11) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(max_degree=3, min_degree=5) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleIntegrationConfig(operators=("+", "-", "*")) + config.validate() + + +def test_simple_integration_dataset_deterministic(): + """Test that dataset generates same items with same seed""" + config = SimpleIntegrationConfig(seed=42, size=10) + dataset1 = SimpleIntegrationDataset(config) + dataset2 = SimpleIntegrationDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_simple_integration_dataset_items(): + """Test that dataset items are valid""" + config = SimpleIntegrationConfig(seed=42, size=10) + dataset = SimpleIntegrationDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + assert "integrand" in item["metadata"] + assert "variable" in item["metadata"] + assert "expected_answer_expression" in item["metadata"] + + # Verify answer is a mathematical expression + answer = item["answer"] + answer = answer.replace(" + C", "") + assert isinstance(parse_expr(answer), sympy.Expr) + + +def test_verify_answer(): + config = SimpleIntegrationConfig(seed=42) + dataset = SimpleIntegrationDataset(config) + for i in range(len(dataset)): + item = dataset[i] + score = dataset.score_answer(item["answer"], item["metadata"]) + assert score == 1.0 + + +def test_score_answer_cases(): + """Test various answer scoring scenarios""" + config = SimpleIntegrationConfig(seed=42) + dataset = SimpleIntegrationDataset(config) + x = sympy.Symbol("x") + X = sympy.Symbol("X") + + # Test cases: (answer, metadata, expected_score) + test_cases = [ + # Correct answers + ("x**2 + C", {"variable": "x", "integrand": "2*x"}, 1.0), + ("X**3 - 5*X + C", {"variable": "X", "integrand": "3*X**2 - 5"}, 1.0), + ("sin(x) + C", {"variable": "x", "integrand": "cos(x)"}, 1.0), + # Correct without explicit constant + ("x**2", {"variable": "x", "integrand": "2*x"}, 1.0), + ("log(x)", {"variable": "x", "integrand": "1/x"}, 1.0), + # Incorrect but properly formatted + ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.05), + # Malformed expressions + ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.01), + ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.01), + # Empty answer + ("", {"variable": "x", "integrand": "2*x"}, 0.01), + # Case sensitivity + ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.05), + ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + # Alternative constant notation + ("x**2 + K", {"variable": "x", "integrand": "2*x"}, 1.0), + ("sin(x) + D", {"variable": "x", "integrand": "cos(x)"}, 1.0), + # Simplification required + ("x**2 + C + 5 - 5", {"variable": "x", "integrand": "2*x"}, 1.0), + ("(x**3)/3 - 2*x + C", {"variable": "x", "integrand": "x**2 - 2"}, 1.0), + ] + + for answer, metadata, expected in test_cases: + score = dataset.score_answer(answer, metadata) + assert score == expected, f"Failed case: {answer} | Expected {expected}, got {score}" diff --git a/tests/test_time_intervals.py b/tests/test_time_intervals.py new file mode 100644 index 00000000..4e95f778 --- /dev/null +++ b/tests/test_time_intervals.py @@ -0,0 +1,113 @@ +from datetime import date, datetime + +import pytest + +from reasoning_gym.arithmetic import TimeIntervalsConfig, TimeIntervalsDataset + + +def test_time_intervals_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = TimeIntervalsConfig(size=0) + config.validate() + + with pytest.raises(AssertionError): + config = TimeIntervalsConfig(max_time_difference_seconds=0) + config.validate() + + with pytest.raises(AssertionError): + config = TimeIntervalsConfig(max_date_difference_days=0) + config.validate() + + with pytest.raises(AssertionError): + config = TimeIntervalsConfig(min_date=date(2024, 1, 1), max_date=date(2023, 1, 1)) + config.validate() + + +def test_time_intervals_deterministic(): + """Test that dataset generates same items with same seed""" + config = TimeIntervalsConfig(seed=42, size=10) + dataset1 = TimeIntervalsDataset(config) + dataset2 = TimeIntervalsDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_time_intervals_items(): + """Test basic properties of generated items""" + config = TimeIntervalsConfig( + size=100, + seed=42, + max_time_difference_seconds=3600, # 1 hour max + max_date_difference_days=10, + ) + dataset = TimeIntervalsDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + assert "task_type" in item["metadata"] + assert "start_time" in item["metadata"] + assert "end_time" in item["metadata"] + + +def test_time_intervals_scoring(): + """Test the answer scoring functionality""" + config = TimeIntervalsConfig(seed=42) + dataset = TimeIntervalsDataset(config) + + # Generate a sample item + item = dataset[0] + + # Test exact match + assert dataset.score_answer(item["answer"], item) == 1.0 + + # Test empty/None answers + assert dataset.score_answer(None, item) == 0.0 + assert dataset.score_answer("", item) == 0.0 + + # Test invalid format + assert dataset.score_answer("invalid", item) == 0.0 + + # Test close but not exact answers + task_type = item["metadata"]["task_type"] + if task_type == "date": + expected = int(item["answer"]) + # Test answer off by 1 day + score = dataset.score_answer(str(expected + 1), item) + assert 0 < score < 1 + elif task_type.startswith("time"): + # Test answer off by a few minutes + if ":" in item["answer"]: + parts = item["answer"].split(":") + hours = int(parts[0]) + minutes = (int(parts[1]) + 5) % 60 # Add 5 minutes + modified = f"{hours:02d}:{minutes:02d}" + if len(parts) > 2: + modified += ":" + parts[2] + score = dataset.score_answer(modified, item) + assert 0 < score < 1 + + +def test_time_format_patterns(): + """Test that generated times match expected formats""" + config = TimeIntervalsConfig(seed=42, size=500) + dataset = TimeIntervalsDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + + start_dt = item["metadata"]["start_time"] + end_dt = item["metadata"]["end_time"] + + # Verify both are datetime objects + assert isinstance(start_dt, datetime) + assert isinstance(end_dt, datetime) + + # Verify end is after start + assert end_dt >= start_dt, item["question"] + assert dataset.score_answer(item["answer"], item) == 1.0 diff --git a/tests/test_tower_of_hanoi.py b/tests/test_tower_of_hanoi.py new file mode 100644 index 00000000..a4228bc3 --- /dev/null +++ b/tests/test_tower_of_hanoi.py @@ -0,0 +1,228 @@ +"""Tests for Tower of Hanoi puzzle generation""" + +import re + +import pytest + +from reasoning_gym.games.tower_of_hanoi import HanoiConfig, HanoiDataset + + +def test_toh_config_validation(): + """Test that invalid configurations raise appropriate errors.""" + # Test negative number of disks + with pytest.raises(AssertionError): + config = HanoiConfig(min_disks=0) # At least 1 disk required + config.validate() + + # Test max_disks less than min_disks + with pytest.raises(AssertionError): + config = HanoiConfig(min_disks=5, max_disks=3) + config.validate() + + # Test min_pegs less than 3 + with pytest.raises(AssertionError): + config = HanoiConfig(min_pegs=2) + config.validate() + + # Test max_pegs less than min_pegs + with pytest.raises(AssertionError): + config = HanoiConfig(min_pegs=3, max_pegs=2) + config.validate() + + # Test invalid move configurations if any (assuming such validations exist) + # Add more tests based on the actual validation logic in HanoiConfig + + +def test_toh_dataset_deterministic(): + """Test that dataset generates the same items with the same seed.""" + config = HanoiConfig(seed=42, size=10) + dataset1 = HanoiDataset(config) + dataset2 = HanoiDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i], f"Mismatch found in instance {i} with seed 42." + + +def test_toh_dataset_items(): + """Test basic properties of generated items.""" + config = HanoiConfig(min_disks=3, max_disks=5, min_pegs=3, max_pegs=4, size=10, seed=42) + dataset = HanoiDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + + # Check item structure + assert isinstance(item, dict), f"Item {i} is not a dictionary." + assert "question" in item, f"Item {i} missing 'question' key." + assert "answer" in item, f"Item {i} missing 'answer' key." + assert "metadata" in item, f"Item {i} missing 'metadata' key." + + # Check metadata + metadata = item["metadata"] + assert "num_disks" in metadata, f"Item {i} metadata missing 'num_disks'." + assert "num_pegs" in metadata, f"Item {i} metadata missing 'num_pegs'." + assert "start_peg" in metadata, f"Item {i} metadata missing 'start_peg'." + assert "target_peg" in metadata, f"Item {i} metadata missing 'target_peg'." + assert "auxiliary_pegs" in metadata, f"Item {i} metadata missing 'auxiliary_pegs'." + assert "solution_length" in metadata, f"Item {i} metadata missing 'solution_length'." + + num_disks = metadata["num_disks"] + num_pegs = metadata["num_pegs"] + start_peg = metadata["start_peg"] + target_peg = metadata["target_peg"] + auxiliary_pegs = metadata["auxiliary_pegs"] + solution_length = metadata["solution_length"] + + # Verify peg counts + assert num_pegs == len(metadata["auxiliary_pegs"]) + 2, f"Item {i} has inconsistent peg counts." + + # Verify solution_length consistency + assert solution_length == len( + item["answer"] + ), f"Item {i} metadata 'solution_length' does not match actual number of moves." + + # Optional: Additional checks like verifying that start and target pegs are distinct + assert start_peg != target_peg, f"Item {i} has identical start and target pegs." + + +def test_toh_move_validity(): + """Test that all moves in each problem instance are valid according to Tower of Hanoi rules.""" + config = HanoiConfig(min_disks=3, max_disks=5, min_pegs=3, max_pegs=4, size=10, seed=42) + dataset = HanoiDataset(config) + + for idx, instance in enumerate(dataset): + num_disks = instance["metadata"]["num_disks"] + num_pegs = instance["metadata"]["num_pegs"] + start_peg = instance["metadata"]["start_peg"] + target_peg = instance["metadata"]["target_peg"] + auxiliary_pegs = instance["metadata"]["auxiliary_pegs"] + pegs = list(range(1, num_pegs + 1)) + + # Initialize pegs_state: all disks start on the start peg + pegs_state = {peg: [] for peg in pegs} + for disk in range(num_disks, 0, -1): + pegs_state[start_peg].append(disk) + + # Iterate over each move and validate + for move_num, move in enumerate(instance["answer"], start=1): + disk, from_peg, to_peg = parse_move(move) + + # Check that from_peg exists + assert from_peg in pegs, f"Move {move_num} in Instance {idx} references non-existent from_peg {from_peg}." + + # Check that to_peg exists + assert to_peg in pegs, f"Move {move_num} in Instance {idx} references non-existent to_peg {to_peg}." + + # Check that from_peg is not empty + assert pegs_state[ + from_peg + ], f"Move {move_num} in Instance {idx} attempts to move from an empty Peg {from_peg}." + + # Check that the disk to move is on top of from_peg + top_disk = pegs_state[from_peg][-1] + assert disk == top_disk, ( + f"Move {move_num} in Instance {idx} attempts to move disk {disk} " + f"which is not on top of Peg {from_peg} (top disk: {top_disk})." + ) + + # Check that moving disk to to_peg does not violate size constraints + if pegs_state[to_peg]: + top_to_disk = pegs_state[to_peg][-1] + assert top_to_disk > disk, ( + f"Move {move_num} in Instance {idx} attempts to place disk {disk} " + f"on top of smaller disk {top_to_disk} on Peg {to_peg}." + ) + + # Perform the move + pegs_state[from_peg].pop() + pegs_state[to_peg].append(disk) + + +def test_toh_final_state_correct(): + """Test that the final state of each problem instance has all disks on the target peg in correct order.""" + config = HanoiConfig(min_disks=3, max_disks=5, min_pegs=3, max_pegs=4, size=10, seed=42) + dataset = HanoiDataset(config) + + for idx, instance in enumerate(dataset): + num_disks = instance["metadata"]["num_disks"] + num_pegs = instance["metadata"]["num_pegs"] + start_peg = instance["metadata"]["start_peg"] + target_peg = instance["metadata"]["target_peg"] + auxiliary_pegs = instance["metadata"]["auxiliary_pegs"] + pegs = list(range(1, num_pegs + 1)) + + # Initialize pegs_state: all disks start on the start peg + pegs_state = {peg: [] for peg in pegs} + for disk in range(num_disks, 0, -1): + pegs_state[start_peg].append(disk) + + # Perform all moves + for move in instance["answer"]: + disk, from_peg, to_peg = parse_move(move) + pegs_state[from_peg].pop() + pegs_state[to_peg].append(disk) + + # After all moves, all disks should be on target peg in descending order + final_pegs = pegs_state[target_peg] + assert len(final_pegs) == num_disks, f"Instance {idx} does not have all disks on the target Peg {target_peg}." + + # Verify that disks are in correct order on target peg + expected_final = list(range(num_disks, 0, -1)) + assert final_pegs == expected_final, f"Instance {idx} has disks on Peg {target_peg} in incorrect order." + + # Ensure all other pegs are empty + for peg in pegs: + if peg != target_peg: + assert ( + len(pegs_state[peg]) == 0 + ), f"Instance {idx} has disks remaining on Peg {peg}, which should be empty." + + +def test_toh_dataset_iteration(): + """Test that iteration respects dataset size and multiple iterations yield the same items.""" + config = HanoiConfig(min_disks=3, max_disks=5, min_pegs=3, max_pegs=4, size=5, seed=42) + dataset = HanoiDataset(config) + + # Test dataset size + assert len(dataset) == config.size, f"Dataset size mismatch: expected {config.size}, got {len(dataset)}." + + # Collect items + items = list(dataset) + + # Test multiple iterations yield the same items + assert items == list(dataset), "Multiple iterations over the dataset do not yield the same items." + + +def parse_move(move_str: str) -> tuple: + """Parse a move string and extract disk number, from peg, and to peg. + + Args: + move_str (str): Move instruction, e.g., "Move disk 2 from Peg 1 to Peg 3". + + Returns: + tuple: (disk, from_peg, to_peg) + """ + pattern = r"Move disk (\d+) from Peg (\d+) to Peg (\d+)" + match = re.match(pattern, move_str) + assert match is not None, f"Move string '{move_str}' does not match the expected format." + disk = int(match.group(1)) + from_peg = int(match.group(2)) + to_peg = int(match.group(3)) + return disk, from_peg, to_peg + + +def is_valid_final_state(pegs_state: dict, target_peg: int, num_disks: int) -> bool: + """Verify that all disks are on the target peg in descending order. + + Args: + pegs_state (dict): Current state of the pegs. + target_peg (int): The target peg number. + num_disks (int): Total number of disks. + + Returns: + bool: True if valid, False otherwise. + """ + target_stack = pegs_state[target_peg] + if len(target_stack) != num_disks: + return False + return target_stack == list(range(num_disks, 0, -1)) diff --git a/tests/test_word_ladder.py b/tests/test_word_ladder.py index b69445fe..3e7e0ddf 100644 --- a/tests/test_word_ladder.py +++ b/tests/test_word_ladder.py @@ -391,4 +391,4 @@ def test_word_ladder_edge_cases(): if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__])