Implement generators #57
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Consider this code
Plan
1. Data model (
include/akbasic/environment.h)Add to
akbasic_Environment:bool isGenerator— set on the environment aGENcall pushes.struct akbasic_Environment *forGeneratorEnv— lives on theFOR EACH/DO EACHloop's own environment, pointing at the detached-but-alive generator environment
between iterations.
NULLmeans "not an EACH loop" or "generator exhausted."forNextVariablefor theEACHvariable.No new field needed to resume a generator: each environment already tracks its own
nextline.2. Split
akbasic_runtime_prev_environment()(src/runtime.c)akbasic_runtime_detach_environment(obj)— returns control to->parentwithoutfreeing (no variable-release, no
used = false).akbasic_runtime_release_environment(obj, env)— the variable-release +used = falsepart, callable on an environment that isn't necessarily
obj->environmentright now.akbasic_runtime_prev_environment(obj)becomesdetach+releasein sequence —every existing GOSUB/RETURN/DEF/FOR/NEXT caller is unaffected.
3. Grammar / scanner
New keywords:
GEN,END GEN,EMIT,EACH— add to verb table (src/verbs.c/verbs.h) and scanner keyword list (src/scanner.c).GENgets an AST shape mirroringmulti-line
DEF(name + parameter list; body scanned toEND GEN).EACHmust beparseable after both
FORandDO.4.
GEN/END GENModeled on multi-line
DEF/RETURN: armsakbasic_environment_wait_for_command(env, "END GEN")so the body is skipped on firstpass, only running when invoked via
FOR EACH/DO EACH(not the expression-call pathused by
AKBASIC_MAX_FUNCTIONS). The EACH branch ofcmd_for/cmd_dopushes anenvironment (parent = the loop's own environment), marks
isGenerator = true, bindsparams like a function call, sets
nextlineto the first line of theGENbody.5.
EMIT(akbasic_cmd_emit, structurally close toakbasic_cmd_return)obj->environment->isGenerator(error otherwise).obj->environment->parent->forNextVariable.obj->environment->parent->nextlineto theFOR EACH/DO EACHloop body start.akbasic_runtime_detach_environment(obj)(notprev_environment) so thegenerator environment survives, still referenced by the loop's
forGeneratorEnv.6.
FOR EACH ... IN ...parsing (akbasic_parse_for)FORmust be a variable orEACH, else syntax error.EACH: parseEACH <var> IN <generator-call-expr>, stash the call expression onthe pushed environment instead of
forToLeaf/forStepLeaf.EACHaccepts any emitted type (STRING, structure element, etc.) — assignmentbypasses
evaluate_for_condition's arithmetic machinery. The numeric-only type-checkfor
NEXT <var>stays on the plainFORpath only.akbasic_cmd_forEACH branch:GENby name infunctions(shared table withDEF; one lookup, one"unknown function/generator" error path; names still can't collide across
GEN/DEF).parentfrom the current environmentlooking for an active, reachable
isGeneratorenvironment with matching functionidentity. If found, error (self-recursion). A detached environment sitting in another
loop's
forGeneratorEnvis a sibling, not an ancestor, so independent/nestedFOR EACH/DO EACHover the sameGENis unaffected.EMIT(detaches back here) orEND GENwith noEMIT(empty generator, zero iterations).akbasic_environment_wait_for_command(obj->environment, "NEXT")on the loop environment.
7.
NEXT <var>— EACH branch (akbasic_cmd_next)Ahead of the existing numeric-condition logic: if
obj->environment->forGeneratorEnv != NULL:obj->environment = forGeneratorEnv, resume atforGeneratorEnv->nextline(
parentis unchanged sinceEMITonly detached).EMIT(repeat) or realEND GEN.END GEN:akbasic_runtime_release_environment()the generator env, clearforGeneratorEnvon the loop environment, pop the loop environment too, handnextlineto whatever followsNEXT— same as today'sEXIT-into-NEXTpath.8.
DO ... LOOP EACH ... IN ...Reuses sections 1–7 directly:
forGeneratorEnvlives on theDO's own environment the same way.akbasic_parse_dogets the sameEACH <var> IN <generator-call-expr>branch asakbasic_parse_for, stashing the call expression alongside/instead ofdoConditionLeaf/doConditionKind.DO EACHandDO WHILE/UNTILare mutuallyexclusive on the same
DO.akbasic_cmd_dogains the same zero-iteration check ascmd_for: push generator, runto first
EMITor immediateEND GEN→wait_for_command(obj->environment, "LOOP").akbasic_cmd_loopgains the same check ascmd_next: ifobj->environment->forGeneratorEnv != NULL, reactivate and resume at itsnextline,ahead of (not blended with) the
WHILE/UNTIL/bare-LOOPlogic (never reached byDO EACH).DOenvironmentexactly as section 7, landing on whatever follows
LOOP.EXITneeds no new branch — already dispatches onobj->environment->isDoLoop, whichDO EACHsets andFOR EACHdoesn't.Net new work: parser plumbing (
akbasic_parse_doacceptingEACH) plus the twoforGeneratorEnvchecks incmd_do/cmd_loopmirroringcmd_for/cmd_next.9. Abandoned generators (early
EXIT,GOTOout of the loop, error unwind)Wherever a loop environment is popped/released (
EXIT's path incmd_next, theequivalent path in
cmd_loop, any error-unwind path walking->parentreleasingscopes): check
forGeneratorEnv != NULLand release it too viaakbasic_runtime_release_environment(). Otherwise it leaks in theAKBASIC_MAX_ENVIRONMENTSpool. This is the main new failure mode — needs dedicatedtest cases for both loop shapes.
10. Design decisions
GENandDEFshare thefunctionstable, at least to start.DO ... LOOP EACH: in scope for v1.FOR EACH/DO EACHcan emit any type; numeric-only check stays onplain
NEXTonly.FOR EACH/DO EACHover the sameGEN(even same name, different args) is explicitlyallowed — each invocation is a fresh pool environment.
EXIT: unchanged, dispatches onisDoLoopexactly as today.11. Testing
GEN ROOMOBJECTS/FOR EACHsnippet from this issue, checked against expectedemitted order.
DO EACH ... LOOP, same expected order.END GEN, noEMIT) → zero iterations, no crash, both loop shapes.EXITout ofFOR EACHandDO EACHpartway through → generator env released, poolnot leaked (loop
AKBASIC_MAX_ENVIRONMENTS+1 such constructs in one test per shape).GENemitting a non-numeric value (e.g.STRING) viaFOR EACH/DO EACH→ succeeds.FOR EACH/DO EACH(legitimate): outer loop body containing innerFOR EACH/DO EACH, including both invoking the sameGENwith different args → both runto completion, correct interleaved emitted values, no false rejection, no pool
corruption.
GENthatFOR EACH/DO EACHs back over itself from itsown still-running invocation → clean error, no corrupted environment stack.
EMIToutside aGENbody, and mismatched-NEXTvariable case → errors like theexisting mismatched-
NEXTcase.GENinvoked without being consumed viaFOR EACH/DO EACH(e.g. called like afunction) → clean error, no stack corruption.
12. Documentation
Undefined behavior
@tachikoma give me a plan to implement this
@tachikoma update the plan for a few things:
@tachikoma i disagree here
I don’t want to allow a generator to call back into itself (recursion). I do want to allow nested ‘FOR EACH’ or ‘DO … LOOP EACH’ constructs. Even if each of those constructs call the same generator (hopefully with different arguments - but that’s a documentation issue), each invocation is a new instance with a new environment. So the environment chaining still must be accounted for.
@tachikoma compact the plan please, ditch the history and focus on the implementation.
Compacted the plan in the issue body — trimmed the narrative/rationale asides and "today's behavior" explanations, kept all 12 implementation sections (data model, environment split, grammar, GEN/END GEN, EMIT, FOR EACH, NEXT, DO LOOP EACH, abandoned-generator cleanup, design decisions, testing, docs) plus the undefined-behavior notes. No code changed.
@tachikoma execute this ticket please, it's assigned to you and ready
Beginning work on this ticket: implementing generators (GEN/END GEN/EMIT/FOR EACH/DO EACH) per the plan in the ticket body.
Work is done. Implemented GEN/EMIT/END GEN/FOR EACH/DO EACH per the plan above, with full test coverage (123/123 ctest passing) and documentation updates.
PR: #61