feat(variables): the engine evaluates parametric exercises (refs #220) - #225
Merged
Merged
Conversation
The engine validated parametric exercises (schema 1.14) and left
sampling, evaluation and substitution to each consumer, so the reference
app carried a second parser for the same expression language.
- evaluateExpression(expression, values) on the parser the validator
uses: it now builds a small syntax tree, parseVariableExpression reads
the names from it, evaluateExpression computes it. Parse-error
messages are unchanged (E-VAR-EXPR passes them as parseError).
- resolveExerciseVariables(exercise, { random, values }) samples,
evaluates in declaration order, rounds to the display precision and
substitutes every reference to a declared variable; returns the
exercise, the values (to persist and replay) and the tolerances of
pure-reference accepted answers. The contract is the app's.
The app's own 30 test cases pass unchanged; a differential run against
the app's module (400 seeded runs) shows 0 differences. Two deliberate
ones follow from one grammar: what the validator rejects (.5, unary +)
does not evaluate, and {{ a }} with spaces, which the validator reads as
a reference, is substituted (the app left it literal).
Part 2 of #220, the grade lookup, waits for its first consumer.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 25, 2026
Merged
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part 1 of #220: the engine evaluates parametric exercises. Part 2, the grade lookup, waits for its first consumer, as the issue says; the issue stays open for it.
resolveExerciseVariables(exercise, { random, values }), in the newsrc/resolve-variables.ts, on the package root.{ exercise, values, toleranceByAcceptText }.valuesreplays a previous attempt.variablescomes back as the same object.evaluateExpression(expression, values)invariables.ts, on the parser the validator uses. The parser now builds a small syntax tree:parseVariableExpressionreads the names from it andevaluateExpressioncomputes it. One grammar, one implementation. The parse-error messages are unchanged, becauseE-VAR-EXPRpasses them asparseError.resolve-exercise-variables.ts), so the app can switch without changing its call site (ExerciseDispatcher.tsx).Two differences from the app, both following from one grammar:
.5, a unary+) does not evaluate either. The app's tokenizer accepted both. Valid content never has them, becauseE-VAR-EXPRrejects them first.{{ a }}, which the validator reads asaand accepts, is substituted. The app left it literal, so such content passed validation and showed raw braces to the learner. The same holds for a pure-reference accept entry{{ sum }}and its tolerance.Division by zero gives the IEEE value (
Infinity/NaN), as in the app. That is documented; a lint for it would be separate work.Tests
src/resolve-variables.test.ts, 39 tests, RED first (the module was missing).sampleVariable,evaluateExpression,formatVariableValueandresolveExerciseVariables.{{ a }}and{{ sum }}with spaces;{{...}}that is not a declared name stays; thevariablesblock is kept; exports on the root.variables.test.tsandissue-params.test.tspass unchanged after the parser refactor.develop) and the engine function ran with the same seeded random stream on 8 synthetic parametric exercises × 50 seeds. 400 runs, 0 differences. A seeded probe with{{ a }}makes all 400 differ, so the check is not blind. There are no parametric exercises in the ten content repositories yet (0 found).make release-check,make prose-check,npm run docs:api:checkandscripts/check-bundle-size.mjspass. The core is at 88.1 of 90 kB gzip, so no budget raise was needed.Docs
lesson-format.md: the Variables section describes the resolver and drops "never samples or evaluates".architecture.md: open item for The engine evaluates what it defines: variable expressions now, the grade lookup with its first consumer #220.comparative-analysis.md§6.3.CHANGELOG.md.Downstream (App lane)
The app replaces
frontend/src/lib/exercises/variables/resolve-exercise-variables.tswith the engine'sresolveExerciseVariables(same contract), and keeps its test file as a smoke test against the engine.🤖 Generated with Claude Code