When two recipes reference each other, cook shopping-list silently double-counts ingredients instead of detecting the cycle. There is no warning and the exit code is 0.
Reproduction
mkdir -p config
printf 'Make @./b{} and add @salt{1%%tsp}.\n' > a.cook
printf 'Make @./a{} and add @pepper{1%%tsp}.\n' > b.cook
printf '[spices]\nsalt\npepper\n' > config/aisle.conf
cook shopping-list a.cook
[spices]
salt 2 tsp
pepper 1 tsp
salt is declared once, in a.cook. It is counted twice because expanding a pulls in b, and expanding b pulls in a again.
Why it terminates but is still wrong
Reference expansion is bounded by depth rather than by cycle detection, so it does not hang — it just walks the cycle a fixed number of times and adds every ingredient it meets. The deeper the mutual references, the further the quantities drift from the truth.
A shopping list with silently wrong quantities is arguably worse than an error, because there is nothing to notice.
Suggested direction
Track the set of recipes already expanded along the current path and stop when one repeats — either erroring, or warning and expanding it once. The seen map that existed for this purpose was effectively inert, because entries were removed before each return.
Noticed while extracting shopping_list into a library crate. The extraction preserves the current behaviour exactly and pins it in a test (mutually_referencing_recipes_terminate), so changing it is a deliberate decision rather than an accident.
When two recipes reference each other,
cook shopping-listsilently double-counts ingredients instead of detecting the cycle. There is no warning and the exit code is 0.Reproduction
saltis declared once, ina.cook. It is counted twice because expandingapulls inb, and expandingbpulls inaagain.Why it terminates but is still wrong
Reference expansion is bounded by depth rather than by cycle detection, so it does not hang — it just walks the cycle a fixed number of times and adds every ingredient it meets. The deeper the mutual references, the further the quantities drift from the truth.
A shopping list with silently wrong quantities is arguably worse than an error, because there is nothing to notice.
Suggested direction
Track the set of recipes already expanded along the current path and stop when one repeats — either erroring, or warning and expanding it once. The
seenmap that existed for this purpose was effectively inert, because entries were removed before each return.Noticed while extracting
shopping_listinto a library crate. The extraction preserves the current behaviour exactly and pins it in a test (mutually_referencing_recipes_terminate), so changing it is a deliberate decision rather than an accident.