From c39ebc094751d45d0ccad107e06e62eea66b762d Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Fri, 9 Jan 2026 15:02:01 +0900 Subject: [PATCH 1/8] feat: relax ordering constraints for $-variable ModuleOptions --- check/features.frm | 13 +++++++++++++ sources/compiler.c | 25 +++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/check/features.frm b/check/features.frm index a1b91032..c31efefa 100644 --- a/check/features.frm +++ b/check/features.frm @@ -2233,6 +2233,19 @@ assert stdout =~ exact_pattern("Generated terms = 1001 ( 1 K )") assert stdout =~ exact_pattern("Terms in output = 1001 ( 1 K )") assert stdout =~ exact_pattern("Bytes used = 199172 (199 KiB)") *--#] humanstats : +*--#[ ModuleOption_dollar_order : +$a = 0; +ModuleOption sum,$a; +$b = 0; +ModuleOption maximum,$b; +$c = 0; +ModuleOption minimum,$c; +$d = 0; +ModuleOption local,$d; +discard; +.end +assert succeeded? +*--#] ModuleOption_dollar_order : *--#[ Issue49 : * Add mul_ function for polynomial multiplications Symbols x,y,z; diff --git a/sources/compiler.c b/sources/compiler.c index dcaa3c0e..8532f2a0 100644 --- a/sources/compiler.c +++ b/sources/compiler.c @@ -43,6 +43,7 @@ */ #include "form3.h" +#include "comtool.h" /* com1commands are the commands of which only part of the word has to @@ -617,8 +618,28 @@ int CompileStatement(UBYTE *in) } else if ( k->type == MIXED2 ) {} else if ( k->type > AC.compiletype ) { - if ( StrCmp((UBYTE *)(k->name),(UBYTE *)"format") != 0 ) - AC.compiletype = k->type; + /* + * We intentionally do NOT update "compiletype" for: + * - Format statements (type = TOOUTPUT) + * - ModuleOption statements (type = ATENDOFMODULE) + * with sum/maximum/minimum/local (i.e., $-variable-related options) + * + * This relaxes the ordering constraint, allowing statements with + * type >= the current "compiletype" to follow. + */ + if ( StrCmp((UBYTE *)(k->name),(UBYTE *)"format") == 0 ) + goto NoUpdateCompileType; + if ( StrCmp((UBYTE *)(k->name),(UBYTE *)"moduleoption") == 0 ) { + UBYTE *ss = s; + SkipSpaces(&ss); + if ( ConsumeOption(&ss,"sum") + || ConsumeOption(&ss,"maximum") + || ConsumeOption(&ss,"minimum") + || ConsumeOption(&ss,"local") ) goto NoUpdateCompileType; + } + AC.compiletype = k->type; +NoUpdateCompileType: + ; } else if ( k->type < AC.compiletype ) { switch ( k->type ) { From bb8f54c7fd977f6b4a4a31410e20a3581f6c3b8b Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Tue, 13 Jan 2026 17:34:17 +0900 Subject: [PATCH 2/8] docs: statement ordering exceptions Provide a more precise description of mixed statements. Add descriptions for Format and ModuleOption. --- doc/manual/module.tex | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/doc/manual/module.tex b/doc/manual/module.tex index 425f22d6..fc7abf10 100644 --- a/doc/manual/module.tex +++ b/doc/manual/module.tex @@ -31,7 +31,7 @@ \chapter{Modules} \item [Definitions\index{definitions}] These define new expressions. \item [Executable\index{executable statements} statements] The operations on all active expressions. -\item [OutputSpecifications\index{output specifications}] These specify the +\item [Output specifications\index{output specifications}] These specify the output representation. \item [End-of-module specifications\index{end of module specifications}] Extra settings that are for this module only. @@ -39,11 +39,34 @@ \chapter{Modules} classes. Most notably the print statement. \end{description} Statements must occur in such an order that no statement follows a -statement of a later category. The only exception is formed by the mixed -statements, which can occur anywhere. This is different from earlier +statement of a later category, except as described bellow. +This is different from earlier versions of \FORM\ in which the order of the statements was not fixed. This did cause a certain amount of confusion about the workings of \FORM\@. +The exceptions are: +\begin{itemize} +\item Mixed statements are permitted in the executable statements, + output specifications, and end-of-module specifications parts of a module. +\item \texttt{Format}\index{format} statements are permitted anywhere. +\item \texttt{ModuleOption}\index{moduleoption} statements that control how \$-variables are handled + during parallel execution are permitted anywhere. +\end{itemize} +The last exception allows such \texttt{ModuleOption} statements to appear inside procedures +that use \$-variables and do not contain a \texttt{.sort}. +For example, the following structure is valid: +\begin{verbatim} + #procedure SortlessProc + $procLocalDollar = ... + ModuleOption local $procLocalDollar; + * more executable statements here + #endprocedure + + #call SortlessProc + * more executable statements here + .sort +\end{verbatim} + There are several types of modules. \begin{description} \item[.sort\index{.sort}] \label{instrsort} The general end-of-module. From 585a0cb397d5e8ff22275eaf3ceec4838a6b9f00 Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Thu, 8 Jan 2026 15:47:54 +0900 Subject: [PATCH 3/8] refactor: remove unused if-branch for MIXED2 --- sources/compiler.c | 1 - sources/ftypes.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sources/compiler.c b/sources/compiler.c index 8532f2a0..04529b87 100644 --- a/sources/compiler.c +++ b/sources/compiler.c @@ -616,7 +616,6 @@ int CompileStatement(UBYTE *in) AC.compiletype = STATEMENT; } } - else if ( k->type == MIXED2 ) {} else if ( k->type > AC.compiletype ) { /* * We intentionally do NOT update "compiletype" for: diff --git a/sources/ftypes.h b/sources/ftypes.h index b3b354a4..beeead24 100644 --- a/sources/ftypes.h +++ b/sources/ftypes.h @@ -161,7 +161,7 @@ #define STATEMENT 4 #define TOOUTPUT 5 #define ATENDOFMODULE 6 -#define MIXED2 8 +#define MIXED2 8 /* unused */ #define MIXED 9 /* From 3aeb8ba231d7651c42480ad5436e1779f6dc9de6 Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Tue, 13 Jan 2026 07:29:07 +0900 Subject: [PATCH 4/8] perf: remove unnecessary null-character check in ConsumeOption --- sources/comtool.h | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/sources/comtool.h b/sources/comtool.h index 02cd1c09..7ae23199 100644 --- a/sources/comtool.h +++ b/sources/comtool.h @@ -42,10 +42,15 @@ */ /** - * Skips white-spaces in the buffer. Here the white-spaces includes commas, - * which is treated as a space in FORM. + * Skips over whitespace characters in the buffer, including commas, + * which are treated as whitespace characters in the FORM compiler. * - * @param[in,out] s The pointer to the buffer. + * @note To avoid skipping commas, use `SKIPBLANKS(s)` instead. + * + * @param[in,out] s Pointer to the current position in the buffer. The buffer + * must be null-terminated. On return, the pointer is + * advanced to the first non-whitespace character, or the + * null terminator if none is found. */ static inline void SkipSpaces(UBYTE **s) { @@ -55,13 +60,15 @@ static inline void SkipSpaces(UBYTE **s) } /** - * Checks if the next word in the buffer is the given keyword, with ignoring - * case. If found, the pointer is moved such that the keyword is consumed in the - * buffer, and this function returns a non-zero value. + * Checks whether the next token in the buffer is the given keyword, + * ignoring case. If found, the keyword is consumed and the pointer is advanced + * to the first non-whitespace character following the keyword. * - * @param[in,out] s The pointer to the buffer. Changed if the keyword found. - * @param opt The optional keyword. - * @return 1 if the keyword found, otherwise 0. + * @param[in,out] s Pointer to the current position in the buffer. + * The buffer must be null-terminated. On return, + * the pointer is advanced if the keyword is found. + * @param opt Case-insensitive keyword. + * @return 1 if the keyword is found, otherwise 0. */ static inline int ConsumeOption(UBYTE **s, const char *opt) { @@ -73,10 +80,9 @@ static inline int ConsumeOption(UBYTE **s, const char *opt) /* Check if `opt` ended. */ if ( !*opt ) { /* Check if `*p` is a word boundary. */ - if ( !*p || !(FG.cTable[(unsigned char)*p] == 0 || - FG.cTable[(unsigned char)*p] == 1 || *p == '_' || - *p == '$') ) { - /* Consume the option. Skip the trailing spaces. */ + UINT c = FG.cTable[(unsigned char)*p]; + if ( c != 0 && c != 1 && *p != '_' && *p != '$' ) { + /* Consume the option. Skip the trailing whitespace. */ *s = (UBYTE *)p; SkipSpaces(s); return(1); From 7561cb16b869b3c6988035e279b97b5047e92bb5 Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Thu, 15 Jan 2026 20:02:18 +0900 Subject: [PATCH 5/8] docs: add comments for some parser routines --- sources/compiler.c | 25 +++++++++++++++++++------ sources/ftypes.h | 7 +++++-- sources/tools.c | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/sources/compiler.c b/sources/compiler.c index 04529b87..0fee5287 100644 --- a/sources/compiler.c +++ b/sources/compiler.c @@ -424,19 +424,32 @@ int ParenthesesTest(UBYTE *sin) /* #] ParenthesesTest : #[ SkipAName : - - Skips a name and gives a pointer to the object after the name. - If there is not a proper name, it returns a zero pointer. - In principle the brackets match already, so the `if ( *s == 0 )' - code is not really needed, but you never know how the program - is extended later. */ +/** + * Skips a name and gives a pointer to the character after the name. + * If there is not a proper name, it emits a compiler message and then returns + * a null pointer. + * This function supports formal names (e.g., `[x+a]`) and $-variables + * in addition to ordinary identifiers. + * + * @note The brackets are assumed to be matched already. + * + * @param[in] s Pointer to a null-terminated input string that starts + * with a name. + * @return Pointer to the character after the name, or a null pointer + * if the name is invalid. + */ UBYTE *SkipAName(UBYTE *s) { UBYTE *t = s; if ( *s == '[' ) { SKIPBRA1(s) +/* + In principle the brackets match already, so the `if ( *s == 0 )' + code is not really needed, but you never know how the program + is extended later. +*/ if ( *s == 0 ) { MesPrint("&Illegal name: '%s'",t); return(0); diff --git a/sources/ftypes.h b/sources/ftypes.h index beeead24..f8a8d753 100644 --- a/sources/ftypes.h +++ b/sources/ftypes.h @@ -187,10 +187,13 @@ typedef int (*TFUN)(); typedef int (*TFUN1)(); #endif +/* + Flags used in the compiler's KEYWORD tables. +*/ #define NOAUTO 0 -#define PARTEST 1 -#define WITHAUTO 2 +#define PARTEST 1 /* parentheses test */ +#define WITHAUTO 2 /* auto-declaration */ #define ALLVARIABLES -1 #define SYMBOLONLY 1 diff --git a/sources/tools.c b/sources/tools.c index 3af8b1e5..c15dd05b 100644 --- a/sources/tools.c +++ b/sources/tools.c @@ -1918,6 +1918,17 @@ UBYTE *strDup1(UBYTE *instring, char *ifwrong) #[ EndOfToken : */ +/** + * Skips over alphanumeric characters to find the end of the token. + * + * @note This function does not handle formal names (e.g., `[x+a]`). + * To handle them, use `SkipAName` instead. + * + * @param[in] s Pointer to a null-terminated input buffer. + * @return Pointer to the first non-alphanumeric character (i.e., the + * character immediately after the token), or the null + * terminator if the token reaches the end of the string. + */ UBYTE *EndOfToken(UBYTE *s) { UBYTE c; @@ -1930,6 +1941,17 @@ UBYTE *EndOfToken(UBYTE *s) #[ ToToken : */ +/** + * Skips over non-alphanumeric characters to find the start of a token. + * + * @note This function does not handle formal names (e.g., `[x+a]`). + * To handle them, consider simply skipping whitespace, e.g., by using + * `SkipSpaces`. + * + * @param[in] s Pointer to a null-terminated input buffer. + * @return Pointer to the first alphanumeric character (i.e., the start + * of the token), or the null terminator if none is found. + */ UBYTE *ToToken(UBYTE *s) { UBYTE c; @@ -1940,11 +1962,17 @@ UBYTE *ToToken(UBYTE *s) /* #] ToToken : #[ SkipField : - - Skips from s to the end of a declaration field. - par is the number of parentheses that still has to be closed. */ - + +/** + * Skips from `s` to the end of a declaration field + * (e.g., `x`, `1+2*[x+a]`, or `f({x,[x+a]},1)`). + * + * @param[in] s Pointer to a null-terminated input buffer. + * @param[in] level Number of parentheses that still have to be closed. + * @return Pointer to the character after the field, which is either + * a comma at level 0 or the null terminator. + */ UBYTE *SkipField(UBYTE *s, int level) { while ( *s ) { From c61d9c7c91c3ddd295e1d906a8e1552a291fefbd Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Tue, 13 Jan 2026 17:54:25 +0900 Subject: [PATCH 6/8] fix: remove unintended & in warnings (#766) Close #766. --- check/fixes.frm | 8 ++++++++ sources/module.c | 2 +- sources/names.c | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/check/fixes.frm b/check/fixes.frm index fcb6ea75..4fe529c2 100644 --- a/check/fixes.frm +++ b/check/fixes.frm @@ -4437,6 +4437,14 @@ assert result("testCF1") =~ expr("putfirst_(f,2,mu1)*putfirst_(e,2,mu1)*putfirst assert result("testCF2") =~ expr("d(mu2,mu1)*e(mu2,mu1)*f(mu2,mu1)") assert result("testCF3") =~ expr("d(mu2,mu1,mu3)*e(mu2,mu1,mu3)*f(mu2,mu1,mu3)") *--#] Issue750 : +*--#[ Issue766 : +* Unintended "&" in some warning messages +CF f(s,s); +ModuleOption local,$a; +.end +assert warning?("Excess information in symmetric properties") +assert warning?("Undefined $-variable") +*--#] Issue766 : *--#[ PullReq535 : * This test requires more than the specified 50K workspace. #:maxtermsize 200 diff --git a/sources/module.c b/sources/module.c index f9aa7453..a5a64115 100644 --- a/sources/module.c +++ b/sources/module.c @@ -672,7 +672,7 @@ UBYTE * DoModDollar(UBYTE *s, int type) number = GetDollar(name); if ( number < 0 ) { number = AddDollar(s,0,0,0); - Warning("&Undefined $-variable in module statement"); + Warning("Undefined $-variable in module statement"); } md = (MODOPTDOLLAR *)FromList(&AC.ModOptDolList); md->number = number; diff --git a/sources/names.c b/sources/names.c index b22d0c73..d8184aff 100644 --- a/sources/names.c +++ b/sources/names.c @@ -1526,7 +1526,7 @@ illegsym: *s = cc; else goto illegsym; *s = cc; if ( *s != ')' || ( s[1] && s[1] != ',' && s[1] != '<' ) ) { - Warning("&Excess information in symmetric properties currently ignored"); + Warning("Excess information in symmetric properties currently ignored"); s = SkipField(s,1); } else s++; @@ -1547,7 +1547,7 @@ retry:; if ( ( StrICont(par,(UBYTE *)"arguments") == 0 ) || ( StrICont(par,(UBYTE *)"args") == 0 ) ) {} else { - Warning("&Illegal information in number of arguments properties currently ignored"); + Warning("Illegal information in number of arguments properties currently ignored"); error = 1; } *s = cc; @@ -1571,7 +1571,7 @@ retry:; if ( ( StrICont(par,(UBYTE *)"arguments") == 0 ) || ( StrICont(par,(UBYTE *)"args") == 0 ) ) {} else { - Warning("&Illegal information in number of arguments properties currently ignored"); + Warning("Illegal information in number of arguments properties currently ignored"); error = 1; } *s = cc; From 8ff3c46598da11e2ba85ecbb2a8a601cccc47f3b Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Fri, 16 Jan 2026 08:40:07 +0900 Subject: [PATCH 7/8] fix: make function "number of arguments" property warnings non-fatal Previously, these warnings caused FORM to terminate with a failure status. --- check/fixes.frm | 3 +++ sources/names.c | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/check/fixes.frm b/check/fixes.frm index 4fe529c2..622b1796 100644 --- a/check/fixes.frm +++ b/check/fixes.frm @@ -4440,9 +4440,12 @@ assert result("testCF3") =~ expr("d(mu2,mu1,mu3)*e(mu2,mu1,mu3)*f(mu2,mu1,mu3)") *--#[ Issue766 : * Unintended "&" in some warning messages CF f(s,s); +CF f>=x<=x; ModuleOption local,$a; .end +assert return_value == 0 assert warning?("Excess information in symmetric properties") +assert warning?("Illegal information in number of arguments properties") assert warning?("Undefined $-variable") *--#] Issue766 : *--#[ PullReq535 : diff --git a/sources/names.c b/sources/names.c index d8184aff..a0d19620 100644 --- a/sources/names.c +++ b/sources/names.c @@ -1548,7 +1548,6 @@ retry:; || ( StrICont(par,(UBYTE *)"args") == 0 ) ) {} else { Warning("Illegal information in number of arguments properties currently ignored"); - error = 1; } *s = cc; } @@ -1572,7 +1571,6 @@ retry:; || ( StrICont(par,(UBYTE *)"args") == 0 ) ) {} else { Warning("Illegal information in number of arguments properties currently ignored"); - error = 1; } *s = cc; } From 7bffd7f812ebd682cc5026bb72730cea5ec94cfc Mon Sep 17 00:00:00 2001 From: Takahiro Ueda Date: Thu, 15 Jan 2026 21:25:04 +0900 Subject: [PATCH 8/8] fix: incorrect error message in InParallel statement --- sources/compcomm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/compcomm.c b/sources/compcomm.c index 781be599..9d0735b9 100644 --- a/sources/compcomm.c +++ b/sources/compcomm.c @@ -3361,7 +3361,7 @@ int DoInParallel(UBYTE *s, int par) *s = c; } else { - MesPrint("&Illegal object in InExpression statement"); + MesPrint("&Illegal object in InParallel statement"); error = 1; while ( *s && *s != ',' ) s++; if ( *s == 0 ) break;