Skip to content

feat(variables): the engine evaluates parametric exercises (refs #220) - #225

Merged
astrapi69 merged 1 commit into
mainfrom
feat/220-evaluate-variables
Sep 25, 2026
Merged

astrapi69 merged 1 commit into
mainfrom
feat/220-evaluate-variables

Conversation

@astrapi69

Copy link
Copy Markdown
Owner

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 new src/resolve-variables.ts, on the package root.
    • It samples every sampled variable with the injected random source and evaluates computed variables in declaration order.
    • It rounds each value to its display precision and substitutes every reference to a declared variable in every string field.
    • It returns { exercise, values, toleranceByAcceptText }. values replays a previous attempt.
    • An exercise without variables comes back as the same object.
  • evaluateExpression(expression, values) in variables.ts, on the parser the validator uses. The parser now builds a small syntax tree: parseVariableExpression reads the names from it and evaluateExpression computes it. One grammar, one implementation. The parse-error messages are unchanged, because E-VAR-EXPR passes them as parseError.
  • The contract is the app's (adaptive-learner#3109 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:

  1. An expression the validator rejects (.5, a unary +) does not evaluate either. The app's tokenizer accepted both. Valid content never has them, because E-VAR-EXPR rejects them first.
  2. A reference with spaces, {{ a }}, which the validator reads as a and 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).
    • 30 of them are the app's own test cases, ported unchanged as the parity oracle: sampleVariable, evaluateExpression, formatVariableValue and resolveExerciseVariables.
    • Plus: every expression the validator accepts evaluates; what it rejects throws, with the parse error in the message; division by zero; {{ a }} and {{ sum }} with spaces; {{...}} that is not a declared name stays; the variables block is kept; exports on the root.
  • The existing variables.test.ts and issue-params.test.ts pass unchanged after the parser refactor.
  • Differential check: the app's module (bundled from 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:check and scripts/check-bundle-size.mjs pass. The core is at 88.1 of 90 kB gzip, so no budget raise was needed.

Docs

Downstream (App lane)

The app replaces frontend/src/lib/exercises/variables/resolve-exercise-variables.ts with the engine's resolveExerciseVariables (same contract), and keeps its test file as a smoke test against the engine.

🤖 Generated with Claude Code

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>
@astrapi69
astrapi69 merged commit ce4d57b into main Sep 25, 2026
4 checks passed
@astrapi69
astrapi69 deleted the feat/220-evaluate-variables branch September 25, 2026 15:47
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.

2 participants