From 3ef7c44c28bdf7faa361a5ea80ab51685343be9c Mon Sep 17 00:00:00 2001 From: henz Date: Thu, 13 Aug 2026 07:33:55 +0800 Subject: [PATCH] feat(repeat): revive repeat_pattern as generic repeat_apply Rune's repeat_pattern applied a closure to a value n times, but it was rune-specific (asserted the result stayed a Rune after each step) even though the underlying n-times-application logic is generic. It was removed from the rune bundle in #888. Bring it back as repeat_apply in the repeat module, with no rune dependency, and with arguments reordered to (func, n, x) to match the existing repeat(func, n) signature. --- .../repeat/src/__tests__/index.test.ts | 35 ++++++++++++++++++- src/bundles/repeat/src/functions.ts | 16 +++++++++ src/bundles/repeat/src/index.ts | 21 +++++++++-- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/bundles/repeat/src/__tests__/index.test.ts b/src/bundles/repeat/src/__tests__/index.test.ts index 046dc0edc8..dc6af4530b 100644 --- a/src/bundles/repeat/src/__tests__/index.test.ts +++ b/src/bundles/repeat/src/__tests__/index.test.ts @@ -7,7 +7,7 @@ import { runAsyncGenerator } from '@sourceacademy/modules-testplugin'; import { describe, expect, it } from 'vitest'; -import { repeat, thrice, twice } from '../functions'; +import { repeat, repeat_apply, thrice, twice } from '../functions'; async function makePlusOne(handler: TestDataHandler) { return closureFromFunction( @@ -78,3 +78,36 @@ describe(repeat, () => { .rejects.toThrow('repeat: Expected integer ≥ 0, got 1.5.'); }); }); + +describe(repeat_apply, () => { + it('applies a closure to a value n times', async () => { + const handler = new TestDataHandler(); + const plusOne = await makePlusOne(handler); + const result = await runAsyncGenerator( + repeat_apply(handler, plusOne, numberValue(5), numberValue(0)) + ); + + expect(result.value).toEqual(5); + }); + + it('returns x unchanged when n = 0', async () => { + const handler = new TestDataHandler(); + const plusOne = await makePlusOne(handler); + const result = await runAsyncGenerator( + repeat_apply(handler, plusOne, numberValue(0), numberValue(5)) + ); + + expect(result.value).toEqual(5); + }); + + it('throws an error when provided a negative or non-integer n', async () => { + const handler = new TestDataHandler(); + const plusOne = await makePlusOne(handler); + + await expect(runAsyncGenerator(repeat_apply(handler, plusOne, numberValue(-1), numberValue(0)))) + .rejects.toThrow('repeat_apply: Expected integer ≥ 0, got -1.'); + + await expect(runAsyncGenerator(repeat_apply(handler, plusOne, numberValue(1.5), numberValue(0)))) + .rejects.toThrow('repeat_apply: Expected integer ≥ 0, got 1.5.'); + }); +}); diff --git a/src/bundles/repeat/src/functions.ts b/src/bundles/repeat/src/functions.ts index 8ee962b763..d24deeb690 100644 --- a/src/bundles/repeat/src/functions.ts +++ b/src/bundles/repeat/src/functions.ts @@ -29,6 +29,22 @@ export function repeat_internal(f: UnaryFunction, n: number): UnaryFunctio return n === 0 ? x => x : x => func(repeat_internal(func, n - 1)(x)); } +/** + * Applies the specified closure to a value n times, threading the result of each + * application into the next. + */ +export async function* repeat_apply(evaluator: IDataHandler, func: TypedValue, n: TypedValue, x: TypedValue): AsyncGenerator, undefined> { + if (!Number.isInteger(n.value) || n.value < 0) { + throw new EvaluatorRuntimeError(`repeat_apply: Expected integer ≥ 0, got ${n.value}.`); + } + + let current = x; + for (let i = 0; i < n.value; i += 1) { + current = yield* evaluator.closure_call_unchecked(func, [current]); + } + return current; +} + /** * Returns a new closure which when applied to an argument, has the same effect * as applying the specified closure to the same argument n times. diff --git a/src/bundles/repeat/src/index.ts b/src/bundles/repeat/src/index.ts index 195449b409..4f05ebf87c 100644 --- a/src/bundles/repeat/src/index.ts +++ b/src/bundles/repeat/src/index.ts @@ -10,11 +10,11 @@ import { BaseModulePlugin, moduleMethod } from '@sourceacademy/conductor/module' import type { IInterfacableEvaluator } from '@sourceacademy/conductor/runner'; import { DataType, type TypedValue } from '@sourceacademy/conductor/types'; -import { repeat as repeat_func, thrice as thrice_func, twice as twice_func } from './functions'; +import { repeat as repeat_func, repeat_apply as repeat_apply_func, thrice as thrice_func, twice as twice_func } from './functions'; export default class RepeatModulePlugin extends BaseModulePlugin { id = 'repeat'; - override exportedNames = ['repeat', 'twice', 'thrice'] as const; + override exportedNames = ['repeat', 'repeat_apply', 'twice', 'thrice'] as const; static override channelAttach = []; constructor(conduit: IConduit, channels: IChannel[], evaluator: IInterfacableEvaluator) { super(conduit, channels, evaluator); @@ -36,6 +36,23 @@ export default class RepeatModulePlugin extends BaseModulePlugin { return yield* repeat_func(this.evaluator, func, n); } + /** + * Applies the specified function to a value n times, threading the result of each + * application into the next. + * @example + * ``` + * repeat_apply(x => x + 2, 5, 0); // Returns 10 + * ``` + * @param func the function to be applied + * @param n the number of times to apply the function + * @param x the initial value + * @returns the result of applying func to x n times + */ + @moduleMethod([DataType.CLOSURE, DataType.NUMBER, DataType.ANY], DataType.ANY) + async* repeat_apply(func: TypedValue, n: TypedValue, x: TypedValue): AsyncGenerator, undefined> { + return yield* repeat_apply_func(this.evaluator, func, n, x); + } + /** * Returns a new function which when applied to an argument, has the same effect * as applying the specified function to the same argument 2 times.