Skip to content

Add 15 practice exercises, and fix 3 example solutions for V 0.5.2 - #307

Closed
metif12 wants to merge 19 commits into
exercism:mainfrom
metif12:add-new-exercises
Closed

metif12 wants to merge 19 commits into
exercism:mainfrom
metif12:add-new-exercises

Conversation

@metif12

@metif12 metif12 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

What

Three independent groups of commits, so each can be reviewed and merged on its
own:

  1. 3 fixes — example solutions and test files that no longer compiled
    against a current V.
  2. 1 reformat — v fmt -w over every exercise file.
  3. 15 new practice exercises — all previously missing from the V track.

Verified with V 0.5.2 c111a52 (vlang/v master) on Windows.
bin/test is green on all 129 exercises, configlet lint passes, and
v fmt -l is clean on every touched file. v vet reports no new diagnostics:
the pre-existing exercises this touches already emitted the same doc warnings
on main.

The track goes from 114 to 129 practice exercises.


1. Fixes for V 0.5.2

Exercise Problem Fix
allergies sorted_with_compare() on a []Allergen makes V emit calls to the builtin compare(int*, int*); the C backend rejects the pointer types. run_test.v sorts a mapped []int instead, so no custom compare is needed.
intergalactic-transmission u8 << int is now widened to int, so add_parity(current) received an int where a u8 is required. Cast the packed byte back with u8(...).
zebra-puzzle constraint !in queue.array() — the in operator on an array of structs generates an int vs struct comparison in C. Replaced with an explicit .any() predicate over the fields that identify a constraint.

2. Formatting

v fmt -w output for 30 files. Most of it is whitespace — the 0.5.2 formatter
aligns the values of consecutive struct fields and drops leading blank lines in
empty bodies — but it is not whitespace-only, and git diff -w 49d40df~1 49d40df
shows 7 files with changes beyond whitespace:

File Beyond-whitespace change
grains, hello-world, pangram, space-age .meta/example.v string literals normalised from "…" to '…'
leap .meta/example.v redundant parens dropped around an if expression used as a value
kindergarten-garden .meta/example.v trailing comma added to an argument list
intergalactic-transmission stub leading blank line dropped from an empty body

All four kinds are behaviour-preserving, but flagging them here so the commit is
not dropped on the assumption that it is pure whitespace.

Kept as its own commit so it can be dropped independently.

grade-school.v and robot-name.v are deliberately skipped, because the
formatter cannot be run over them safely: grade-school.v fails to parse at
all, and although robot-name.v parses, v fmt rewrites its <TYPE>
placeholders into garbage (fn create_robot(mut robots <TYPE>) becomes
fn create_robot(mut robots , TYPE, )). The .meta/example.v of both was
formatted normally, since it has no placeholders.

3. New exercises (15)

Exercise Difficulty Tests Notes
save-the-cow 2 10 State enum + mut struct; errors for guessing after the game ends
relative-distance 3 7 BFS; siblings count as one degree apart
dnd-character 4 19 V truncates integer division toward zero, so the ability modifier needs an explicit round-down
prism 4 14 Laser/ray tracing with f64; needs a collinearity tolerance because the canonical data rounds coordinates
simple-cipher 4 12 Vigenere; the canonical data uses the old nested v2 format and gets flattened
zipper 4 14 type Tree = Empty | Branch sum type plus a path of steps
bank-account 5 17 spawn + sync.Mutex; V's concurrency is a good fit here
markdown 5 17 Refactoring exercise — the stub is deliberately poor but working
tree-building 5 16 Refactoring exercise; recursive Node struct, error messages match the canonical strings
react 6 14 Cells, reaction, fixed-point settling, change-only callbacks
sgf-parsing 6 23 Recursive-descent parser with SGF text escaping
alphametics 7 10 Column-wise backtracking
poker 8 37 Hand scoring as a category + tiebreak vector
forth 8 55 Interpreter; definition bodies are expanded eagerly so a redefinition does not change existing words
baffling-birthdays 8 17 Monte Carlo birthday-paradox estimate (see below)

Every suite has exactly one fn test_*() per canonical case that is included
(tests.toml minus include = false), which matches the convention used
elsewhere in the track. dnd-character skips 1 excluded case, sgf-parsing
skips 1, poker skips 2.


Points worth a reviewer's attention

  • baffling-birthdays is inherently random. The canonical data is a
    probability estimate, so the reference solution is a Monte Carlo simulation
    and the tests average 25 runs against a 1.5 percentage-point tolerance
    (~4.7σ, roughly 1e-6 failure odds per assertion; 30 consecutive green runs
    observed). If you would rather not carry a probabilistic test in CI, this is
    the one to drop from the PR.
  • markdown and tree-building are refactoring exercises, so their stubs
    are complete but deliberately poor implementations instead of empty bodies.
    That follows the problem-specifications guidance ("the expectation is that
    the stub file has a poorly written but working implementation"), but this
    track has no other exercise of that shape yet. Both stubs pass the suite
    unchanged — note that bin/test only exercises .meta/example.v, so the
    stubs have to be tested separately.
  • prism needs a collinearity tolerance of about 0.1. The canonical
    coordinates are rounded to one decimal and the angles to three, so the
    intended beam is never exactly collinear. Both the stub and the example
    document this.
  • V 0.5.2 has an intermittent type-checker crash that is not caused by this
    PR: types__resolve_type_name_for_method: RUNTIME ERROR: invalid memory access, roughly 1 run in 40, and it reproduces on main too. When it hits,
    the assertions still all pass and only the exit code is non-zero. If
    bin/test reports a failure on an exercise this PR did not touch, re-run it
    before investigating.

Reviewing

The three sections are separate commits. git rebase -i upstream/main lets you
drop or reorder any of them, and every exercise is a single self-contained
commit if you would prefer one PR per exercise.

- allergies: replace sorted_with_compare() in run_test.v. V 0.5.2 emits
  calls to the builtin compare(int*, int*) when a custom compare function
  is used with a non-int element type, which fails in the C backend.
  Sorting a mapped []int instead avoids the broken codegen.
- intergalactic-transmission: u8 shift results are now widened to int,
  so cast the packed byte back to u8 before handing it to add_parity().
- zebra-puzzle: the 'in' operator on an array of structs generates a
  comparison between int and the struct in C. Use an explicit any()
  predicate comparing the fields that identify a constraint.
\� fmt\ in V 0.5.2 aligns the values of consecutive struct fields and
drops leading blank lines inside empty function bodies. Running it over
every exercise file keeps the track consistent with the formatter that
CI and contributors get from a current V install.
Both exercises follow the track conventions: a snake_case stub in the
exercise root, an example solution in .meta, and a run_test.v suite built
from the canonical data in problem-specifications.
@github-actions

Copy link
Copy Markdown
Contributor

This PR touches files which potentially affect the outcome of the tests of an exercise. This will cause all students' solutions to affected exercises to be re-tested.

If this PR does not affect the result of the test (or, for example, adds an edge case that is not worth rerunning all tests for), please add the following to the merge-commit message which will stops student's tests from re-running. Please copy-paste to avoid typos.

[no important files changed]

For more information, refer to the documentation. If you are unsure whether to add the message or not, please ping @exercism/maintainers-admin in a comment. Thank you!

@keiravillekode

Copy link
Copy Markdown
Contributor

For Now

This is a very large PR. Let's work on the first commit in a new PR, and then we can discuss creating subsequent PRs after that PR is merged.

We run exercise tests in a test runner.

bin/verify-exercises-in-docker

Your fixes in bb8fb8ee421ef071ec257fa002895f7720bc7bdb pass with this. Excellent.

For Later

I checked if your proposed exercise additions also work with our current test runner

The follow did not.

  • baffling-birthdays
  • bank-account
  • react
  • save-the-cow
  • sgf-parsing
  • tree-building

The second work item might be to update the test runner to a more recent V release.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Splitting this up as suggested. The first commit is now on its own: #308.

It is verified on both ends of the supported range — bin/verify-exercises-in-docker (V 0.4.8) and V 0.5.2 for bin/test in CI.

For the 15 new exercises: I reproduced the six failures and they are all source-level incompatibilities with the V 0.4.8 that the test runner pins, not test-runner limitations:

  • tree-building, sgf-parsing — !void is not accepted
  • save-the-cow — an array literal of char literals is inferred as []rune
  • react — the captured copy of a variable is immutable in a closure
  • baffling-birthdays — an infinite for {} counts as a missing return
  • bank-account — lock is a reserved keyword, and passing a &sync.WaitGroup to a spawned function as mut miscompiles on 0.5.2

Each has a small, behaviour-preserving equivalent that compiles on 0.4.8 and 0.5.2 alike, so the track can stay on the current test runner. I will send the exercises as separate PRs — the nine that already pass first, then the six adjusted ones — starting after #308 is merged.

This PR stays open for the reformat and the exercises.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

First exercise PR, split out as one self-contained commit: #309 (relative-distance).

Built from add-new-exercises, with the config.json registration reduced to just this exercise. Verified with bin/verify-exercises-in-docker (V 0.4.8) and with V 0.5.2 for bin/test; configlet lint, v fmt -l and v vet are clean.

Next, in difficulty order: simple-cipher, zipper, dnd-character, prism, markdown, alphametics, poker, forth — then the six that needed adjusting.

@keiravillekode

Copy link
Copy Markdown
Contributor

Let's work on the first commit in a new PR, and then we can discuss creating subsequent PRs after that PR is merged.

It might be merged soon. The next step is 'discuss creating subsequent PRs'

First exercise PR, split out as one self-contained commit: #309 (relative-distance).

Please be patient.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

The remaining eight exercises that already work with the current test runner are now up as one PR each, each a single self-contained commit:

Each was verified with bin/verify-exercises-in-docker (the current vlang-test-runner image, V 0.4.8) and with V 0.5.2 for bin/test; configlet lint, v fmt -l and v vet are clean. In every PR the config.json registration is reduced to that one exercise, so they can be merged in any order.

What is left in this PR is the v fmt reformat and the six exercises that needed adjusting for V 0.4.8: save-the-cow, bank-account, tree-building, react, sgf-parsing and baffling-birthdays. I will keep those separate so this one can be reduced to just the reformat, if you prefer.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

The six exercises that did not build with the V 0.4.8 of the current test runner are up as one PR each. Every one of them is a small, behaviour-preserving adjustment, so the track does not need a V upgrade in the test runner:

The causes, in short:

Exercise What 0.4.8 rejects What the PR does
save-the-cow an array literal of char literals is inferred as []rune the test helper takes the letters as a string and calls .bytes()
bank-account lock is a reserved keyword, so neither the field nor mutex.lock() parses the field is mutex, the method is called @lock(), balance takes its receiver by value, and the test waits on a buffered channel instead of a sync.WaitGroup
tree-building, sgf-parsing !void !
react calling a mut method on a captured variable in a closure the callback is a method value and Recorder is @[heap]
baffling-birthdays a bare for {} is not an exhaustive loop for day > 0 with a return after the loop

All six are verified with bin/verify-exercises-in-docker (the current image, V 0.4.8) and with V 0.5.2 for bin/test; configlet lint, v fmt -l and v vet are clean.

One thing worth a decision: baffling-birthdays asserts a probability, averaging 25 runs against a 1.5 percentage-point tolerance. It has been green on every run so far, but if a probabilistic test does not belong on the track, #323 is the one to drop.

That leaves the v fmt reformat in this PR, as a single whitespace-only commit. It is safe on V 0.4.8 as well -- I spot-checked several of the reformatted exercises against the current test runner image -- and its merge commit should carry [no important files changed].

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

The last piece is up on its own: #324, the v fmt reformat. Every exercise it touches passes the current test runner image and V 0.5.2, and the commit message spells out what v fmt 0.5.2 actually changes -- including that it rewrites "..." as '...', so it is not whitespace-only as my original description here said. The outcomes are the same either way, and the merge commit should carry [no important files changed].

That completes all 16 PRs from this branch: #308 (three fixes), #309 to #317 (nine exercises), #318 to #323 (six exercises), #324 (reformat).

One unrelated thing turned up while verifying #324, and it is not caused by any of these PRs -- it is on main today:

V reports a test named test_verse__single_verse__first_generic_verse as main.test_verse.single_verse.first_generic_verse(), because V turns __ into . in a test name. The test runner reads the names out of run_test.v and then looks for exactly those names in the output, so the two never line up and every one of those tests comes back as "Test did not run". The overall status is still pass, so it goes unnoticed.

It affects any exercise with a __ in a test function name:

  • binary-search-tree: 8
  • bottle-song: 7
  • dominoes: 3
  • high-scores: 9
  • house: 12
  • kindergarten-garden: 17
  • pig-latin: 23
  • robot-simulator: 18
  • run-length-encoding: 13
  • state-of-tic-tac-toe: 29
  • twelve-days: 15
  • two-bucket: 6

That is 160 tests where a student gets no per-test feedback from the test runner. I can open an issue and a fix for it separately, since renaming the test functions would change the test outcome of those exercises and so should not ride along with any of the PRs above.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Two things came out of verifying #324, both unrelated to the exercise PRs here.

First, a bug that is already on main of the track. V 0.4.8, the version the test runner image pins, prints a test function named test_verse__single_verse__first_generic_verse as main.test_verse.single_verse.first_generic_verse(). The runner takes the test names from run_test.v and looks for exactly those names in the output of v test, so on 0.4.8 every test with a __ in its name comes back as "Test did not run" while the exercise is still reported as passed. 160 tests over 12 exercises are affected, and a student solving pig-latin gets no per-test feedback at all.

It is a bug in the test runner rather than in the track, so I filed it and fixed it there:

With that fix mounted over the current image, all 12 exercises report every test, hello-world is unchanged, and a deliberately failing test with a __ name is still reported as fail.

Second, the baffling-birthdays exercise in #323 asserts a probability, averaging 25 runs against a 1.5 percentage-point tolerance. It has been green on every run, but if a probabilistic test does not belong on the track, that is the PR to drop.

@metif12

metif12 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

I'm done. Now I wait.

@iHiD

iHiD commented Sep 28, 2026

Copy link
Copy Markdown
Member

I'm done. Now I wait.

@metif12 Thanks for your contributions. Please be aware that while it's very fast to generate code with LLMs, we still have to manually read, review and assess it all before it can be merged. And that process takes time.

v vet emits a notice for plain.len > 0 on both the stub and the example.
Behaviour is identical.
@metif12 metif12 mentioned this pull request Oct 1, 2026
@metif12 metif12 closed this Oct 1, 2026
metif12 added a commit to metif12/exercism-vlang that referenced this pull request Oct 2, 2026
Adds the save-the-cow practice exercise (difficulty 2), split out of
exercism#307. The example keeps the game in a `mut` struct with a `State` enum
and rejects a guess that comes after the game is over.

`played` in run_test.v takes the guessed letters as a string and turns
them into bytes itself, instead of taking an array of char literals.
V 0.4.8 -- the version the current test runner image pins -- infers
`[]rune` for an array literal of char literals and then refuses to pass
it where `[]u8` is expected, while V 0.5.2 infers `[]u8` and accepts
it. Passing the letters as a string and calling `.bytes()` reads the
same on every V version, and the test cases are easier to read for it.

Registers the exercise in config.json. Verified with
bin/verify-exercises-in-docker (the current test runner image) and with
V 0.5.2 for bin/test. configlet lint passes, and v fmt -l / v vet are
clean on the new files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants