Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57)
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
224
tests/generators.c
Normal file
224
tests/generators.c
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* @file generators.c
|
||||
* @brief Generators: GEN/EMIT/END GEN and the FOR EACH/DO EACH loops over them.
|
||||
*
|
||||
* The golden corpus (tests/language/flowcontrol/generators_*.bas) covers the
|
||||
* ordinary shapes: the issue's own ROOMOBJECTS example in both loop forms, an
|
||||
* empty generator, a non-numeric EMIT and nested/interleaved invocations. This
|
||||
* file covers what a byte-compared program cannot: that abandoning a
|
||||
* generator with EXIT gives its environment back to the pool rather than
|
||||
* leaking it, and that misusing a GEN fails cleanly rather than corrupting the
|
||||
* environment stack.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
|
||||
#include "harness.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Run a program to completion under an explicit step budget. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, harness_start(NULL));
|
||||
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||||
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, run_program_bounded(source, 20000));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief The issue's own example, as a sanity check independent of the golden corpus. */
|
||||
static void test_room_objects_smoke(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 DIM OBJ#(3)\n"
|
||||
"20 OBJ#(0) = 100\n"
|
||||
"30 OBJ#(1) = 200\n"
|
||||
"40 OBJ#(2) = 300\n"
|
||||
"50 GEN ROOMOBJECTS(R#)\n"
|
||||
"60 FOR I# = 0 TO 2\n"
|
||||
"70 IF I# <> 1 THEN EMIT OBJ#(I#)\n"
|
||||
"80 NEXT I#\n"
|
||||
"90 END GEN\n"
|
||||
"100 FOR EACH O# IN ROOMOBJECTS(0)\n"
|
||||
"110 PRINT O#\n"
|
||||
"120 NEXT O#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "100\n300\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT out of a FOR EACH loop releases its generator, not just the loop.
|
||||
*
|
||||
* Run more FOR EACH/EXIT constructs than AKBASIC_MAX_ENVIRONMENTS -- each one
|
||||
* takes two environments (the loop's own and the generator's) -- in a single
|
||||
* program. If EXIT abandoned the generator environment instead of releasing
|
||||
* it, this exhausts the pool partway through and the run reports "Environment
|
||||
* pool exhausted" instead of finishing.
|
||||
*/
|
||||
static void test_exit_releases_generator_for_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 FOR EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 NEXT V#\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief The same leak check for DO EACH/EXIT. */
|
||||
static void test_exit_releases_generator_do_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 DO EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 LOOP\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT partway through, with more of the generator left to run, still
|
||||
* frees the environment for the next construct that needs one.
|
||||
*/
|
||||
static void test_exit_partway_through(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH V# IN COUNTUP(10)\n"
|
||||
"70 PRINT V#\n"
|
||||
"80 IF V# = 2 THEN EXIT\n"
|
||||
"90 NEXT V#\n"
|
||||
"100 PRINT \"AFTER\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nAFTER\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||
* EACH/DO EACH, fails cleanly.
|
||||
*
|
||||
* EMIT requires `isGenerator`, which only a FOR EACH/DO EACH invocation sets
|
||||
* -- an ordinary call pushes a plain environment, exactly as a DEF's does --
|
||||
* so the first EMIT the call reaches is where this is refused.
|
||||
*/
|
||||
static void test_called_like_a_function(void)
|
||||
{
|
||||
akbasic_Value *args[1];
|
||||
akbasic_Value argvalue;
|
||||
akbasic_Value *out = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||||
"10 GEN ONE(N#)\n"
|
||||
"20 EMIT N#\n"
|
||||
"30 END GEN\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_value_zero(&argvalue));
|
||||
argvalue.valuetype = AKBASIC_TYPE_INTEGER;
|
||||
argvalue.intval = 5;
|
||||
args[0] = &argvalue;
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ONE", args, 1, &out));
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief EMIT reached with no enclosing GEN invocation is refused. */
|
||||
static void test_emit_outside_gen(void)
|
||||
{
|
||||
akbasic_ASTLeaf *leaf = NULL;
|
||||
akbasic_Value *out = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||
TEST_REQUIRE_OK(harness_parse("EMIT 5", &leaf));
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A GEN invoking itself, directly, is refused rather than recursing.
|
||||
*
|
||||
* Bounded tightly: a program that recursed forever would hang the whole
|
||||
* suite, and this is exactly the case that must not.
|
||||
*/
|
||||
static void test_self_recursion_refused(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program_bounded("10 GEN RECURSIVE(N#)\n"
|
||||
"20 FOR EACH X# IN RECURSIVE(N# + 1)\n"
|
||||
"30 EMIT X#\n"
|
||||
"40 NEXT X#\n"
|
||||
"50 END GEN\n"
|
||||
"60 PRINT \"BEFORE\"\n"
|
||||
"70 FOR EACH R# IN RECURSIVE(1)\n"
|
||||
"80 PRINT R#\n"
|
||||
"90 NEXT R#\n"
|
||||
"100 PRINT \"UNREACHABLE\"\n", 2000));
|
||||
/* Whatever else happened, the line before the recursive call ran and the
|
||||
one two lines after invoking it -- which would only print after the
|
||||
loop completed successfully -- did not. */
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "BEFORE\n") != NULL,
|
||||
"expected \"BEFORE\" in \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||
"self-recursion must not reach \"UNREACHABLE\", got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Independent (sibling) FOR EACH invocations of the same GEN are not
|
||||
* self-recursion, even nested.
|
||||
*/
|
||||
static void test_sibling_invocations_allowed(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH A# IN COUNTUP(2)\n"
|
||||
"70 FOR EACH B# IN COUNTUP(2)\n"
|
||||
"80 PRINT A# * 10 + B#\n"
|
||||
"90 NEXT B#\n"
|
||||
"100 NEXT A#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_room_objects_smoke();
|
||||
test_exit_releases_generator_for_each();
|
||||
test_exit_releases_generator_do_each();
|
||||
test_exit_partway_through();
|
||||
test_called_like_a_function();
|
||||
test_emit_outside_gen();
|
||||
test_self_recursion_refused();
|
||||
test_sibling_invocations_allowed();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
10 REM A GEN invoked like an ordinary function is refused, not silently run.
|
||||
20 GEN ONE(N#)
|
||||
30 EMIT N#
|
||||
40 END GEN
|
||||
50 X# = ONE(5)
|
||||
60 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,2 @@
|
||||
? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function
|
||||
|
||||
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
@@ -0,0 +1,14 @@
|
||||
10 REM Same generator as generators_foreach.bas, via DO EACH ... LOOP.
|
||||
20 DIM OBJ#(3)
|
||||
30 OBJ#(0) = 100
|
||||
40 OBJ#(1) = 200
|
||||
50 OBJ#(2) = 300
|
||||
60 GEN ROOMOBJECTS(R#)
|
||||
70 FOR I# = 0 TO 2
|
||||
80 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
90 NEXT I#
|
||||
100 END GEN
|
||||
110 DO EACH O# IN ROOMOBJECTS(0)
|
||||
120 PRINT O#
|
||||
130 LOOP
|
||||
140 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
@@ -0,0 +1,3 @@
|
||||
10 REM EMIT outside any GEN invocation is refused cleanly.
|
||||
20 EMIT 5
|
||||
30 PRINT "UNREACHABLE"
|
||||
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
? 20 : RUNTIME ERROR EMIT outside the context of a GEN body
|
||||
|
||||
11
tests/language/flowcontrol/generators_empty.bas
Normal file
11
tests/language/flowcontrol/generators_empty.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM A GEN with no EMIT at all runs its FOR EACH/DO EACH body zero times.
|
||||
20 GEN NOTHING(N#)
|
||||
30 END GEN
|
||||
40 FOR EACH X# IN NOTHING(0)
|
||||
50 PRINT "NEVER FOR"
|
||||
60 NEXT X#
|
||||
70 PRINT "AFTER FOR"
|
||||
80 DO EACH Y# IN NOTHING(0)
|
||||
90 PRINT "NEVER DO"
|
||||
100 LOOP
|
||||
110 PRINT "AFTER DO"
|
||||
2
tests/language/flowcontrol/generators_empty.txt
Normal file
2
tests/language/flowcontrol/generators_empty.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
AFTER FOR
|
||||
AFTER DO
|
||||
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM The GEN/FOR EACH example from issue #57, with real arrays standing in
|
||||
20 REM for OBJCOUNT%/VISIBLE/OBJ%.
|
||||
30 DIM OBJ#(3)
|
||||
40 OBJ#(0) = 100
|
||||
50 OBJ#(1) = 200
|
||||
60 OBJ#(2) = 300
|
||||
70 GEN ROOMOBJECTS(R#)
|
||||
80 FOR I# = 0 TO 2
|
||||
90 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
100 NEXT I#
|
||||
110 END GEN
|
||||
120 FOR EACH O# IN ROOMOBJECTS(0)
|
||||
130 PRINT O#
|
||||
140 NEXT O#
|
||||
150 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
@@ -0,0 +1,10 @@
|
||||
10 REM A stray NEXT with nothing open behaves like the plain-FOR case: the
|
||||
20 REM FOR EACH's own NEXT closes it cleanly, and a second one errors.
|
||||
30 GEN ONE(N#)
|
||||
40 EMIT N#
|
||||
50 END GEN
|
||||
60 FOR EACH O# IN ONE(1)
|
||||
70 PRINT O#
|
||||
80 NEXT O#
|
||||
90 NEXT O#
|
||||
100 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,3 @@
|
||||
1
|
||||
? 90 : RUNTIME ERROR NEXT outside the context of FOR
|
||||
|
||||
22
tests/language/flowcontrol/generators_nested.bas
Normal file
22
tests/language/flowcontrol/generators_nested.bas
Normal file
@@ -0,0 +1,22 @@
|
||||
10 REM Nested FOR EACH/DO EACH over the same GEN with different arguments,
|
||||
20 REM in every combination of the two loop shapes.
|
||||
30 GEN COUNTUP(N#)
|
||||
40 FOR I# = 1 TO N#
|
||||
50 EMIT I#
|
||||
60 NEXT I#
|
||||
70 END GEN
|
||||
80 FOR EACH A# IN COUNTUP(2)
|
||||
90 FOR EACH B# IN COUNTUP(3)
|
||||
100 PRINT A# * 10 + B#
|
||||
110 NEXT B#
|
||||
120 NEXT A#
|
||||
130 DO EACH C# IN COUNTUP(2)
|
||||
140 DO EACH D# IN COUNTUP(2)
|
||||
150 PRINT C# * 100 + D#
|
||||
160 LOOP
|
||||
170 LOOP
|
||||
180 FOR EACH E# IN COUNTUP(2)
|
||||
190 DO EACH F# IN COUNTUP(2)
|
||||
200 PRINT E# * 1000 + F#
|
||||
210 LOOP
|
||||
220 NEXT E#
|
||||
14
tests/language/flowcontrol/generators_nested.txt
Normal file
14
tests/language/flowcontrol/generators_nested.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
11
|
||||
12
|
||||
13
|
||||
21
|
||||
22
|
||||
23
|
||||
101
|
||||
102
|
||||
201
|
||||
202
|
||||
1001
|
||||
1002
|
||||
2001
|
||||
2002
|
||||
11
tests/language/flowcontrol/generators_string.bas
Normal file
11
tests/language/flowcontrol/generators_string.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM FOR EACH/DO EACH accept any emitted type, not just numeric.
|
||||
20 GEN WORDS(N#)
|
||||
30 EMIT "HELLO"
|
||||
40 EMIT "WORLD"
|
||||
50 END GEN
|
||||
60 FOR EACH W$ IN WORDS(0)
|
||||
70 PRINT W$
|
||||
80 NEXT W$
|
||||
90 DO EACH V$ IN WORDS(0)
|
||||
100 PRINT V$
|
||||
110 LOOP
|
||||
4
tests/language/flowcontrol/generators_string.txt
Normal file
4
tests/language/flowcontrol/generators_string.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
HELLO
|
||||
WORLD
|
||||
HELLO
|
||||
WORLD
|
||||
@@ -66,15 +66,18 @@ int main(void)
|
||||
/*
|
||||
* These rows have no exec handler because another verb's parse path consumes
|
||||
* them; they are never evaluated on their own. `WHILE` and `UNTIL` belong to
|
||||
* DO and LOOP the way `TO` and `STEP` belong to FOR. Any other missing
|
||||
* handler is a verb that would report "Unknown command" at runtime.
|
||||
* DO and LOOP the way `TO` and `STEP` belong to FOR, and `EACH`/`IN` belong
|
||||
* to FOR EACH and DO EACH the same way. Any other missing handler is a verb
|
||||
* that would report "Unknown command" at runtime.
|
||||
*/
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
if ( table[i].exec != NULL ) {
|
||||
continue;
|
||||
}
|
||||
TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 ||
|
||||
strcmp(table[i].name, "EACH") == 0 ||
|
||||
strcmp(table[i].name, "ELSE") == 0 ||
|
||||
strcmp(table[i].name, "IN") == 0 ||
|
||||
strcmp(table[i].name, "NOT") == 0 ||
|
||||
strcmp(table[i].name, "OR") == 0 ||
|
||||
strcmp(table[i].name, "REM") == 0 ||
|
||||
|
||||
Reference in New Issue
Block a user