From d974aaec5e5b006de8ed793e4248579cf13e3f0a Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 26 Jul 2026 08:38:13 -0400 Subject: [PATCH 1/4] feat: add floor/ceiling/round and math functions to NLP++ (3.7.13) NLP++ had only logten, abs, mod, factorial and randomint for math. Add the rounding family and the remaining core C/C++ math functions: floor(num) NUM round down, toward -infinity ceiling(num) NUM round up, toward +infinity round(num) NUM nearest whole number, halfway away from zero truncate(num) NUM discard the fraction, toward zero sqrt(num) FLOAT square root, errors on a negative argument pow(base,exp) FLOAT errors when the result is NaN or out of float range log(num) FLOAT natural (base e) log, argument must be positive The four rounding functions take a float and return a whole number, which is what callers actually want; floor and truncate deliberately differ on negatives (floor(-3.7) = -4, truncate(-3.7) = -3). Registered in the parallel funcs.h / func_defs.h tables (kept in sync, 340 entries each), declared in fn.h, dispatched and implemented in fn.cpp. Compiled analyzers need matching runtime entry points: codegen in iaction.cpp emits Arun:: for every builtin, so each function also gets Arun overloads in fnrun.cpp and declarations in Arun.h, including the four RFASem/float combinations for the two-argument pow. The global C functions are called with the :: qualifier so the new Arun members do not recurse into themselves. Help: seven new pages under visualtext/Help/helps modeled on logten.htm, TOC entries in Help.hhc, and Table_of_Math_Functions.htm rebuilt. That table previously listed logten alone, so abs, factorial, mod and randomint are now documented there as well. Verified against a live analyzer: all values correct, including the floor/truncate split on negatives and round(-3.5) = -4. Note that negative literals as arguments (e.g. floor(-3.2)) fail to parse, but that is a pre-existing NLP++ limitation -- abs(-5), mod(-7,3) and logten(-1.0) fail the same way; passing a negative variable works. Co-Authored-By: Claude Opus 5 (1M context) --- include/Api/lite/Arun.h | 18 + lite/fn.cpp | 236 +++++++++++++ lite/fn.h | 7 + lite/fnrun.cpp | 237 +++++++++++++ lite/func_defs.h | 9 + lite/funcs.h | 8 + visualtext/Help/helps/Help.hhc | 28 ++ .../Help/helps/Table_of_Math_Functions.htm | 310 +++++++++++++++++- visualtext/Help/helps/ceiling.htm | 109 ++++++ visualtext/Help/helps/floor.htm | 109 ++++++ visualtext/Help/helps/log.htm | 109 ++++++ visualtext/Help/helps/pow.htm | 110 +++++++ visualtext/Help/helps/round.htm | 109 ++++++ visualtext/Help/helps/sqrt.htm | 109 ++++++ visualtext/Help/helps/truncate.htm | 109 ++++++ 15 files changed, 1608 insertions(+), 9 deletions(-) create mode 100644 visualtext/Help/helps/ceiling.htm create mode 100644 visualtext/Help/helps/floor.htm create mode 100644 visualtext/Help/helps/log.htm create mode 100644 visualtext/Help/helps/pow.htm create mode 100644 visualtext/Help/helps/round.htm create mode 100644 visualtext/Help/helps/sqrt.htm create mode 100644 visualtext/Help/helps/truncate.htm diff --git a/include/Api/lite/Arun.h b/include/Api/lite/Arun.h index fce54ffe..0d1c8e13 100644 --- a/include/Api/lite/Arun.h +++ b/include/Api/lite/Arun.h @@ -1789,6 +1789,24 @@ class LITE_API Arun static float logten(Nlppp*,RFASem*); // 04/29/04 AM. static float logten(Nlppp*,float); // 04/29/04 AM. + // Math functions. // 07/26/26 DD. + static long long floor(Nlppp*,RFASem*); + static long long floor(Nlppp*,float); + static long long ceiling(Nlppp*,RFASem*); + static long long ceiling(Nlppp*,float); + static long long round(Nlppp*,RFASem*); + static long long round(Nlppp*,float); + static long long truncate(Nlppp*,RFASem*); + static long long truncate(Nlppp*,float); + static float sqrt(Nlppp*,RFASem*); + static float sqrt(Nlppp*,float); + static float log(Nlppp*,RFASem*); + static float log(Nlppp*,float); + static float pow(Nlppp*,RFASem*,RFASem*); + static float pow(Nlppp*,RFASem*,float); + static float pow(Nlppp*,float,RFASem*); + static float pow(Nlppp*,float,float); + static _TCHAR *urltofile(Nlppp*,_TCHAR*,_TCHAR*); // 05/14/14 AM. static _TCHAR *urltofile(Nlppp*,RFASem*,_TCHAR*); // 05/14/14 AM. diff --git a/lite/fn.cpp b/lite/fn.cpp index 4cd7e122..1fba54bc 100644 --- a/lite/fn.cpp +++ b/lite/fn.cpp @@ -15,6 +15,7 @@ All rights reserved. #include //MAXINT,MAXLONG,MAX_INT,MAX_LONG // 01/20/00 AM. #include // For char handling. // 01/06/03 AM. #include // For math, log10. // 04/29/04 AM. +#include // For FLT_MAX. // 07/26/26 DD. //#include // 06/20/00 AM. //#include // 05/19/14 DDH. @@ -418,6 +419,20 @@ switch (fnid) // 12/21/01 AM. return fnAbs(args,nlppp,/*UP*/sem); case FNmod: return fnMod(args,nlppp,/*UP*/sem); + case FNceiling: + return fnCeiling(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNfloor: + return fnFloor(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNlog: + return fnLog(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNpow: + return fnPow(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNround: + return fnRound(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNsqrt: + return fnSqrt(args,nlppp,/*UP*/sem); // 07/26/26 DD. + case FNtruncate: + return fnTruncate(args,nlppp,/*UP*/sem); // 07/26/26 DD. case FNlook: break; case FNlookup: @@ -13259,6 +13274,227 @@ bool Fn::fnMod( return true; } +/******************************************** +* FN: FNFLOOR +* CR: 07/26/26 DD. +* SUBJ: Round a number down to the nearest whole number. +* RET: True if ok, else false. +* FORMS: floor(num) +********************************************/ + +bool Fn::fnFloor( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("floor"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("floor"), parse)) + return false; + + sem = new RFASem((long long)::floor((double)num)); + + return true; +} + +/******************************************** +* FN: FNCEILING +* CR: 07/26/26 DD. +* SUBJ: Round a number up to the nearest whole number. +* RET: True if ok, else false. +* FORMS: ceiling(num) +********************************************/ + +bool Fn::fnCeiling( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("ceiling"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("ceiling"), parse)) + return false; + + sem = new RFASem((long long)::ceil((double)num)); + + return true; +} + +/******************************************** +* FN: FNROUND +* CR: 07/26/26 DD. +* SUBJ: Round a number to the nearest whole number. +* RET: True if ok, else false. +* FORMS: round(num) +* NOTE: Halfway cases round away from zero, as in C/C++ round. +********************************************/ + +bool Fn::fnRound( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("round"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("round"), parse)) + return false; + + sem = new RFASem((long long)::round((double)num)); + + return true; +} + +/******************************************** +* FN: FNTRUNCATE +* CR: 07/26/26 DD. +* SUBJ: Discard the fractional part of a number. +* RET: True if ok, else false. +* FORMS: truncate(num) +* NOTE: Rounds toward zero, unlike floor which rounds toward -infinity. +********************************************/ + +bool Fn::fnTruncate( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("truncate"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("truncate"), parse)) + return false; + + sem = new RFASem((long long)::trunc((double)num)); + + return true; +} + +/******************************************** +* FN: FNSQRT +* CR: 07/26/26 DD. +* SUBJ: Calculate the square root of a number. +* RET: True if ok, else false. +* FORMS: sqrt(num) +********************************************/ + +bool Fn::fnSqrt( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("sqrt"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("sqrt"), parse)) + return false; + + if (num < 0) + { + _stprintf(Errbuf, _T("[sqrt: Error. Negative argument.]")); + return parse->errOut(true); + } + + sem = new RFASem((float)::sqrt((double)num)); + + return true; +} + +/******************************************** +* FN: FNPOW +* CR: 07/26/26 DD. +* SUBJ: Raise a number to the given power. +* RET: True if ok, else false. +* FORMS: pow(base, exponent) +********************************************/ + +bool Fn::fnPow( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float base = 0, expon = 0; + + if (!Arg::float1(_T("pow"), (DELTS*&)args, base)) + return false; + if (!Arg::float1(_T("pow"), (DELTS*&)args, expon)) + return false; + if (!Arg::done((DELTS*)args, _T("pow"), parse)) + return false; + + double result = ::pow((double)base, (double)expon); + if (!(result == result) // NaN, eg negative base to a fractional power. + || result > FLT_MAX || result < -FLT_MAX) + { + _stprintf(Errbuf, _T("[pow: Error. Result undefined or out of range.]")); + return parse->errOut(true); + } + + sem = new RFASem((float)result); + + return true; +} + +/******************************************** +* FN: FNLOG +* CR: 07/26/26 DD. +* SUBJ: Compute the natural (base e) logarithm of a number. +* RET: True if ok, else false. +* FORMS: log(num) +* NOTE: See logten for the base 10 logarithm. +********************************************/ + +bool Fn::fnLog( + Delt *args, + Nlppp *nlppp, + RFASem* &sem + ) +{ + sem = 0; + Parse *parse = nlppp->parse_; + float num = 0; + + if (!Arg::float1(_T("log"), (DELTS*&)args, num)) + return false; + if (!Arg::done((DELTS*)args, _T("log"), parse)) + return false; + + if (num <= 0) + { + _stprintf(Errbuf, _T("[log: Error. Argument must be positive.]")); + return parse->errOut(true); + } + + sem = new RFASem((float)::log((double)num)); + + return true; +} + #ifdef WORDNET /******************************************** diff --git a/lite/fn.h b/lite/fn.h index 0fad9deb..6af3b292 100644 --- a/lite/fn.h +++ b/lite/fn.h @@ -1014,6 +1014,13 @@ static bool fnStruniquechars( static bool fnRandomint(Delt*,Nlppp*,RFASem*&); static bool fnAbs(Delt*,Nlppp*,RFASem*&); static bool fnMod(Delt*,Nlppp*,RFASem*&); + static bool fnCeiling(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnFloor(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnLog(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnPow(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnRound(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnSqrt(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. + static bool fnTruncate(Delt*,Nlppp*,RFASem*&); // 07/26/26 DD. #ifdef WORDNET // WordNet Functions diff --git a/lite/fnrun.cpp b/lite/fnrun.cpp index f874c497..9067a9f6 100644 --- a/lite/fnrun.cpp +++ b/lite/fnrun.cpp @@ -18012,6 +18012,243 @@ return log10(num1); } +/******************************************** +* FN: FLOOR, CEILING, ROUND, TRUNCATE +* CR: 07/26/26 DD. +* SUBJ: Rounding functions for compiled analyzers. +* RET: Whole number as a long long. +* NOTE: The global C functions must be called with the :: qualifier, +* else these would recurse into themselves. +********************************************/ + +long long Arun::floor( + Nlppp *nlppp, + float num1 + ) +{ +return (long long)::floor((double)num1); +} + +long long Arun::floor( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return floor(nlppp,num1); +} + +long long Arun::ceiling( + Nlppp *nlppp, + float num1 + ) +{ +return (long long)::ceil((double)num1); +} + +long long Arun::ceiling( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return ceiling(nlppp,num1); +} + +long long Arun::round( + Nlppp *nlppp, + float num1 + ) +{ +return (long long)::round((double)num1); +} + +long long Arun::round( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return round(nlppp,num1); +} + +long long Arun::truncate( + Nlppp *nlppp, + float num1 + ) +{ +return (long long)::trunc((double)num1); +} + +long long Arun::truncate( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return truncate(nlppp,num1); +} + + +/******************************************** +* FN: SQRT, LOG, POW +* CR: 07/26/26 DD. +* SUBJ: Power and logarithm functions for compiled analyzers. +* RET: Float. +********************************************/ + +float Arun::sqrt( + Nlppp *nlppp, + float num1 + ) +{ +if (num1 < 0) + { + std::_t_strstream gerrStr; + gerrStr << _T("[sqrt: Negative argument.]") << std::ends; + errOut(&gerrStr,false); + return 0; + } +return (float)::sqrt((double)num1); +} + +float Arun::sqrt( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return sqrt(nlppp,num1); +} + +float Arun::log( + Nlppp *nlppp, + float num1 + ) +{ +if (num1 <= 0) + { + std::_t_strstream gerrStr; + gerrStr << _T("[log: Argument must be positive.]") << std::ends; + errOut(&gerrStr,false); + return 0; + } +return (float)::log((double)num1); +} + +float Arun::log( + Nlppp *nlppp, + RFASem *num1_sem + ) +{ +if (!num1_sem) + return 0; +bool ok = false; +float num1 = num1_sem->sem_to_float(ok); +delete num1_sem; +if (!ok) + return 0; +return log(nlppp,num1); +} + +float Arun::pow( + Nlppp *nlppp, + float base, + float expon + ) +{ +double result = ::pow((double)base, (double)expon); +if (!(result == result)) // NaN, eg negative base to a fractional power. + { + std::_t_strstream gerrStr; + gerrStr << _T("[pow: Result undefined.]") << std::ends; + errOut(&gerrStr,false); + return 0; + } +return (float)result; +} + +float Arun::pow( + Nlppp *nlppp, + RFASem *base_sem, + float expon + ) +{ +if (!base_sem) + return 0; +bool ok = false; +float base = base_sem->sem_to_float(ok); +delete base_sem; +if (!ok) + return 0; +return pow(nlppp,base,expon); +} + +float Arun::pow( + Nlppp *nlppp, + float base, + RFASem *expon_sem + ) +{ +if (!expon_sem) + return 0; +bool ok = false; +float expon = expon_sem->sem_to_float(ok); +delete expon_sem; +if (!ok) + return 0; +return pow(nlppp,base,expon); +} + +float Arun::pow( + Nlppp *nlppp, + RFASem *base_sem, + RFASem *expon_sem + ) +{ +if (!base_sem || !expon_sem) + return 0; +bool ok1 = false, ok2 = false; +float base = base_sem->sem_to_float(ok1); +float expon = expon_sem->sem_to_float(ok2); +delete base_sem; +delete expon_sem; +if (!ok1 || !ok2) + return 0; +return pow(nlppp,base,expon); +} + + /******************************************** * FN: ELTNODE * CR: 09/12/06 AM. diff --git a/lite/func_defs.h b/lite/func_defs.h index 67fa7b6c..1bbfe966 100644 --- a/lite/func_defs.h +++ b/lite/func_defs.h @@ -363,6 +363,15 @@ enum funcDef FNfileexists, FNfilesize, + // Math functions. // 07/26/26 DD. + FNceiling, + FNfloor, + FNlog, + FNpow, + FNround, + FNsqrt, + FNtruncate, + FNXXYYZZ }; #endif diff --git a/lite/funcs.h b/lite/funcs.h index 0c65a485..623a53ed 100644 --- a/lite/funcs.h +++ b/lite/funcs.h @@ -355,4 +355,12 @@ _T("deletefile"), _T("direxists"), _T("fileexists"), _T("filesize"), +// Math functions. // 07/26/26 DD. +_T("ceiling"), +_T("floor"), +_T("log"), +_T("pow"), +_T("round"), +_T("sqrt"), +_T("truncate"), 0 }; diff --git a/visualtext/Help/helps/Help.hhc b/visualtext/Help/helps/Help.hhc index 53eca727..fb2069ae 100644 --- a/visualtext/Help/helps/Help.hhc +++ b/visualtext/Help/helps/Help.hhc @@ -1911,10 +1911,38 @@ +
  • + + + +
  • + + + +
  • + + +
  • +
  • + + + +
  • + + + +
  • + + + +
  • + + +
  • diff --git a/visualtext/Help/helps/Table_of_Math_Functions.htm b/visualtext/Help/helps/Table_of_Math_Functions.htm index 286ac1c6..2ed11fcf 100644 --- a/visualtext/Help/helps/Table_of_Math_Functions.htm +++ b/visualtext/Help/helps/Table_of_Math_Functions.htm @@ -38,7 +38,7 @@

    Table of Math Functions

    -

    NLP++ now features mathematical functions analogous to those provided +

    NLP++ now features mathematical functions analogous to those provided in C/C++.

    @@ -93,7 +93,8 @@

    Table of Math Functions

    bgcolor=#00FF00 width=76%>

    DESCRIPTION + color=#000000>DESCRIPTION + -

    logten(num_float) +

    abs(num) + +

    NUM + +

    Get the absolute value of a number. + + + +

    ceiling(num_float) + +

    NUM + +

    Round a number up to the nearest whole number. Analogous to the ceil function in C/C++. + + + +

    factorial(num) + +

    NUM + +

    Get the factorial of a whole number. + + + +

    floor(num_float) + +

    NUM + +

    Round a number down to the nearest whole number. Analogous to the floor function in C/C++. + + + +

    log(num_float) + +

    FLOAT + +

    Get the natural (base e) logarithm of a number. Analogous to the log function in C/C++. + + + +

    logten(num_float) + +

    FLOAT + +

    Get the base 10 logarithm of a number. Analogous to the log10 function in C/C++. + + + +

    mod(num,num) + +

    NUM + +

    Get the remainder after dividing the first number by the second. Analogous to the % operator in C/C++. + + + +

    pow(base_float,exponent_float) + +

    FLOAT + +

    Raise a number to the given power. Analogous to the pow function in C/C++. + + + +

    randomint(num,num) + +

    NUM + +

    Get a random whole number between the two given numbers, inclusive. + + + +

    round(num_float) + +

    NUM + +

    Round a number to the nearest whole number, with halfway cases rounding away from zero. Analogous to the round function in C/C++. + + + +

    sqrt(num_float) -

    Get base 10 logarithm of given number.  Analogous - to log10 function in C/C++. +

    Get the square root of a number. Analogous to the sqrt function in C/C++. -

      +

    truncate(num_float) -

      +

    NUM -

      +

    Discard the fractional part of a number, rounding toward zero. Analogous to the trunc function in C/C++.

    See Also

    +

     

    + - + \ No newline at end of file diff --git a/visualtext/Help/helps/ceiling.htm b/visualtext/Help/helps/ceiling.htm new file mode 100644 index 00000000..13bab27d --- /dev/null +++ b/visualtext/Help/helps/ceiling.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +ceiling + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    ceiling

    + +

    Purpose

    + +

    Round a number up to the nearest whole number.

    + +

    Syntax

    + +

    returnedNum = ceiling(numFlt)

    + +

    returnedNum - type: number
    +numFlt - type: float

    + +

    Returns

    + +

    The smallest whole number that is greater than or equal to the given number.

    + +

    Remarks

    + +

    Analogous to the ceil function in C/C++. Rounds toward positive infinity, so the ceiling of a negative number moves toward zero.

    + +

    Example

    + +

    @CODE
    +"output.txt" << ceiling(3.2) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    4

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/floor.htm b/visualtext/Help/helps/floor.htm new file mode 100644 index 00000000..1c894ab8 --- /dev/null +++ b/visualtext/Help/helps/floor.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +floor + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    floor

    + +

    Purpose

    + +

    Round a number down to the nearest whole number.

    + +

    Syntax

    + +

    returnedNum = floor(numFlt)

    + +

    returnedNum - type: number
    +numFlt - type: float

    + +

    Returns

    + +

    The largest whole number that is less than or equal to the given number.

    + +

    Remarks

    + +

    Analogous to the floor function in C/C++. Rounds toward negative infinity, so the floor of a negative number moves away from zero. Use truncate to discard the fraction and round toward zero instead.

    + +

    Example

    + +

    @CODE
    +"output.txt" << floor(3.7) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    3

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/log.htm b/visualtext/Help/helps/log.htm new file mode 100644 index 00000000..01292ae7 --- /dev/null +++ b/visualtext/Help/helps/log.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +log + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    log

    + +

    Purpose

    + +

    Compute the natural (base e) logarithm of a number.

    + +

    Syntax

    + +

    returnedFlt = log(numFlt)

    + +

    returnedFlt - type: float
    +numFlt - type: float

    + +

    Returns

    + +

    The natural logarithm of the given number.

    + +

    Remarks

    + +

    Analogous to the log function in C/C++. The argument must be positive. See logten for the base 10 logarithm.

    + +

    Example

    + +

    @CODE
    +"output.txt" << log(2.718282) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    1

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/pow.htm b/visualtext/Help/helps/pow.htm new file mode 100644 index 00000000..050caa81 --- /dev/null +++ b/visualtext/Help/helps/pow.htm @@ -0,0 +1,110 @@ + + + + + + + + + + +pow + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    pow

    + +

    Purpose

    + +

    Raise a number to the given power.

    + +

    Syntax

    + +

    returnedFlt = pow(baseFlt,exponentFlt)

    + +

    returnedFlt - type: float
    +baseFlt - type: float
    +exponentFlt - type: float

    + +

    Returns

    + +

    The base raised to the power of the exponent.

    + +

    Remarks

    + +

    Analogous to the pow function in C/C++. A negative base with a fractional exponent is undefined and is reported as an error.

    + +

    Example

    + +

    @CODE
    +"output.txt" << pow(2,10) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    1024

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/round.htm b/visualtext/Help/helps/round.htm new file mode 100644 index 00000000..956b4417 --- /dev/null +++ b/visualtext/Help/helps/round.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +round + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    round

    + +

    Purpose

    + +

    Round a number to the nearest whole number.

    + +

    Syntax

    + +

    returnedNum = round(numFlt)

    + +

    returnedNum - type: number
    +numFlt - type: float

    + +

    Returns

    + +

    The whole number nearest the given number.

    + +

    Remarks

    + +

    Analogous to the round function in C/C++. Halfway cases round away from zero, so round(3.5) is 4 and round(-3.5) is -4.

    + +

    Example

    + +

    @CODE
    +"output.txt" << round(3.5) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    4

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/sqrt.htm b/visualtext/Help/helps/sqrt.htm new file mode 100644 index 00000000..f5f2a72e --- /dev/null +++ b/visualtext/Help/helps/sqrt.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +sqrt + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    sqrt

    + +

    Purpose

    + +

    Compute the square root of a number.

    + +

    Syntax

    + +

    returnedFlt = sqrt(numFlt)

    + +

    returnedFlt - type: float
    +numFlt - type: float

    + +

    Returns

    + +

    The square root of the given number.

    + +

    Remarks

    + +

    Analogous to the sqrt function in C/C++. A negative argument is an error.

    + +

    Example

    + +

    @CODE
    +"output.txt" << sqrt(16) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    4

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file diff --git a/visualtext/Help/helps/truncate.htm b/visualtext/Help/helps/truncate.htm new file mode 100644 index 00000000..2d8dc12b --- /dev/null +++ b/visualtext/Help/helps/truncate.htm @@ -0,0 +1,109 @@ + + + + + + + + + + +truncate + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    truncate

    + +

    Purpose

    + +

    Discard the fractional part of a number.

    + +

    Syntax

    + +

    returnedNum = truncate(numFlt)

    + +

    returnedNum - type: number
    +numFlt - type: float

    + +

    Returns

    + +

    The given number with its fractional part removed.

    + +

    Remarks

    + +

    Analogous to the trunc function in C/C++. Rounds toward zero, so truncate(-3.7) is -3, whereas floor(-3.7) is -4.

    + +

    Example

    + +

    @CODE
    +"output.txt" << truncate(3.7) << "\n";
    +@@CODE

    + +

    Outputs:

    + +

    3

    + +

    See Also

    + +

    Math Functions

    + +

     

    + + + + \ No newline at end of file From da93da00aecc66e48b2557f915d4e9b1c55e3adb Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 26 Jul 2026 09:01:38 -0400 Subject: [PATCH 2/4] chore: bump NLP_ENGINE_VERSION to 3.7.13 The math-function commit named 3.7.13 in its subject but left the #define at 3.7.12, so nlp --version would have under-reported the build. Note that this #define is what --version actually prints; the generated Version() from version.cpp is commented out at the callsite. Co-Authored-By: Claude Opus 5 (1M context) --- nlp/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlp/main.cpp b/nlp/main.cpp index cf3bac76..833fea0b 100644 --- a/nlp/main.cpp +++ b/nlp/main.cpp @@ -15,7 +15,7 @@ All rights reserved. #include "lite/nlp_engine.h" #include "version.h" -#define NLP_ENGINE_VERSION "3.7.12" +#define NLP_ENGINE_VERSION "3.7.13" bool cmdReadArgs(int, _TCHAR *argv[], _TCHAR *&, _TCHAR *&, _TCHAR *&, _TCHAR *&, bool &, bool &, bool &, bool &, bool &, bool &); void cmdHelpargs(_TCHAR *); From f2a87fba4403c3da19fb2bd3f3c02d89921be74f Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 26 Jul 2026 09:14:50 -0400 Subject: [PATCH 3/4] fix: allow digits and underscores in NLP++ function names (#698) A user function whose name contained a digit (Round2, Clamp01, Fixed2) or an underscore (Round_2) never compiled. The rule-file tokenizer built _LIT from a single _xALPHA token, so "Round2" arrived at the function grammar as _LIT("Round") + _NUM(2) and never matched "_FNCALLLIST <- _LIT \(" in gram2.nlp. The diagnostic was "[Warning: Missing ';']" + "[Syntax error.]" on a function header that is itself valid, which sends you looking at the previous statement instead of at the name. Take the fix the issue prefers -- accept the digits -- rather than reporting a better error, since digits have always been legal in L() variable names. bigtok.nlp: glom adjacent alpha/numeric/underscore tokens into one _LIT, but only when they are immediately followed by an UNESCAPED "(", ie a function definition header or a call. That gate is what keeps the change contained: @RULES literals must write "\(" and key=(val) pairs put "=" before the paren, so element literals ("covid19" stays two elements) and pair values tokenize exactly as before. The second element is [plus], so single-token names never reach the rule and the keyword rules (if/while/return/L/N/X/G/P/s) still win. postrfa.cpp: rfaname() gains a two-arg form that gathers the covered text across a range of collects; the one-arg form is untouched. The name still starts at an _xALPHA, so the identifiers Ifunc::genDecls emits into generated C++ stay legal. Verified: all forms from the issue table now run (Round2, R2ound, x2Round, Round_2, Clamp01, plus Round_Two). parse-en-us final.tree is byte-identical to the CI baseline, and an A/B run of all 15 tutorial/nlpfix/corporate analyzers against the unpatched grammar shows zero drift. Adds .github/workflows/tests/fnname-digits plus a step in the Windows and Linux builds next to the existing #669 regression. The step asserts out.txt rather than the exit code -- the engine exits 0 even when the analyzer fails to build. Confirmed the step fails on the unpatched grammar. No version bump: this rides along with 3.7.13 so both changes ship in one build. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-linux.yml | 24 ++++++++ .github/workflows/build-windows.yml | 23 ++++++++ .../tests/fnname-digits/input/text.txt | 1 + .../tests/fnname-digits/kb/user/.gitkeep | 1 + .../tests/fnname-digits/spec/analyzer.seq | 3 + .../tests/fnname-digits/spec/fndigits.nlp | 59 +++++++++++++++++++ .../tests/fnname-digits/spec/lits.nlp | 16 +++++ data/rfb/spec/bigtok.nlp | 22 +++++++ lite/postrfa.cpp | 54 ++++++++++++++++- 9 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests/fnname-digits/input/text.txt create mode 100644 .github/workflows/tests/fnname-digits/kb/user/.gitkeep create mode 100644 .github/workflows/tests/fnname-digits/spec/analyzer.seq create mode 100644 .github/workflows/tests/fnname-digits/spec/fndigits.nlp create mode 100644 .github/workflows/tests/fnname-digits/spec/lits.nlp diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 25046146..aaab1985 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -135,6 +135,30 @@ jobs: -WORK ./ .github/workflows/tests/fulldict-hang/input/dash.txt echo "[#669 regression] analyzer completed without hanging" + - name: Function names with digits regression (#698, Non-20.04) + if: matrix.id != 'docker-ubuntu-20.04' + shell: bash + run: | + set -eo pipefail + # The rule-file tokenizer splits "Round2" into "Round" + "2", so + # user functions whose name held a digit or an underscore never + # matched the "_LIT \(" function grammar and the analyzer died with + # a misleading "[Warning: Missing ';']". The fixture also asserts + # that @RULES literals ("covid19", "a1 \(") still tokenize the old + # way -- the glom must only fire on an unescaped "(". + FIX=.github/workflows/tests/fnname-digits + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + ./bin/nlp -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" + cat "$FIX/logs/make_ana.log" + TREE="$FIX/input/text.txt_log/final.tree" + got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + if [ "$got" != "3 21 201 2001 1 8 71" ]; then + echo "FAIL(#698): out.txt was '$got'"; exit 1 + fi + grep -q '_COVID' "$TREE" || { echo "FAIL(#698): literal covid19 no longer matches two tokens"; exit 1; } + grep -q '_APAREN' "$TREE" || { echo "FAIL(#698): escaped '\\(' literal regressed"; exit 1; } + echo "[#698 regression] digit/underscore function names compile and run" + - name: Copy nlp.exe to nlpl.exe (Non-20.04) if: matrix.id != 'docker-ubuntu-20.04' run: cp bin/nlp bin/nlpl.exe diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 02213789..6e1be8ee 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -61,6 +61,29 @@ jobs: -WORK ./ .github/workflows/tests/fulldict-hang/input/dash.txt echo "[#669 regression] analyzer completed without hanging" + - name: Function names with digits regression (#698) + shell: bash + run: | + set -eo pipefail + # The rule-file tokenizer splits "Round2" into "Round" + "2", so + # user functions whose name held a digit or an underscore never + # matched the "_LIT \(" function grammar and the analyzer died with + # a misleading "[Warning: Missing ';']". The fixture also asserts + # that @RULES literals ("covid19", "a1 \(") still tokenize the old + # way -- the glom must only fire on an unescaped "(". + FIX=.github/workflows/tests/fnname-digits + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + ./bin/Release/nlp.exe -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" + cat "$FIX/logs/make_ana.log" + TREE="$FIX/input/text.txt_log/final.tree" + got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + if [ "$got" != "3 21 201 2001 1 8 71" ]; then + echo "FAIL(#698): out.txt was '$got'"; exit 1 + fi + grep -q '_COVID' "$TREE" || { echo "FAIL(#698): literal covid19 no longer matches two tokens"; exit 1; } + grep -q '_APAREN' "$TREE" || { echo "FAIL(#698): escaped '\\(' literal regressed"; exit 1; } + echo "[#698 regression] digit/underscore function names compile and run" + - name: Copy nlp.exe to nlpw.exe run: cp bin/Release/nlp.exe bin/Release/nlpw.exe shell: bash diff --git a/.github/workflows/tests/fnname-digits/input/text.txt b/.github/workflows/tests/fnname-digits/input/text.txt new file mode 100644 index 00000000..f75c0fe4 --- /dev/null +++ b/.github/workflows/tests/fnname-digits/input/text.txt @@ -0,0 +1 @@ +covid19 a1( x diff --git a/.github/workflows/tests/fnname-digits/kb/user/.gitkeep b/.github/workflows/tests/fnname-digits/kb/user/.gitkeep new file mode 100644 index 00000000..0524ba3d --- /dev/null +++ b/.github/workflows/tests/fnname-digits/kb/user/.gitkeep @@ -0,0 +1 @@ +# Placeholder so git tracks kb/user; the engine needs the dir to exist. diff --git a/.github/workflows/tests/fnname-digits/spec/analyzer.seq b/.github/workflows/tests/fnname-digits/spec/analyzer.seq new file mode 100644 index 00000000..9d880fee --- /dev/null +++ b/.github/workflows/tests/fnname-digits/spec/analyzer.seq @@ -0,0 +1,3 @@ +tokenize nil # tokenize only -- fixture needs no dictionary +nlp fndigits # #698: function names containing digits/underscores +nlp lits # @RULES literals must tokenize as before diff --git a/.github/workflows/tests/fnname-digits/spec/fndigits.nlp b/.github/workflows/tests/fnname-digits/spec/fndigits.nlp new file mode 100644 index 00000000..459ef9de --- /dev/null +++ b/.github/workflows/tests/fnname-digits/spec/fndigits.nlp @@ -0,0 +1,59 @@ +############################################### +# FILE: fndigits.nlp +# SUBJ: Regression for issue #698 -- user function names that contain a +# digit or an underscore. +# NOTE: The rule-file tokenizer splits "Round2" into "Round" + "2", so +# before the fix every one of these definitions died with a +# misleading "[Warning: Missing ';']" + "[Syntax error.]" and the +# analyzer never built. Expected out.txt: "3 21 201 2001 1 8 71". +############################################### + +@DECL + +Round2(L("x")) { + return L("x") + 2; +} + +R2ound(L("x")) { + return L("x") + 20; +} + +x2Round(L("x")) { + return L("x") + 200; +} + +Round_2(L("x")) { + return L("x") + 2000; +} + +Clamp01(L("x")) { + if (L("x") > 1) + return 1; + if (L("x") < 0) + return 0; + return L("x"); +} + +# Digits in LOCAL VARIABLE names always worked. Keep checking them. +RoundTwo(L("x2")) { + L("y2") = L("x2") + 7; + return L("y2"); +} + +Round_Two(L("x")) { + return L("x") + 70; +} + +@@DECL + +@CODE + +"out.txt" << str(Round2(1)) << " "; +"out.txt" << str(R2ound(1)) << " "; +"out.txt" << str(x2Round(1)) << " "; +"out.txt" << str(Round_2(1)) << " "; +"out.txt" << str(Clamp01(5)) << " "; +"out.txt" << str(RoundTwo(1)) << " "; +"out.txt" << str(Round_Two(1)) << "\n"; + +@@CODE diff --git a/.github/workflows/tests/fnname-digits/spec/lits.nlp b/.github/workflows/tests/fnname-digits/spec/lits.nlp new file mode 100644 index 00000000..32ee50b4 --- /dev/null +++ b/.github/workflows/tests/fnname-digits/spec/lits.nlp @@ -0,0 +1,16 @@ +############################################### +# FILE: lits.nlp +# SUBJ: Guard the OTHER half of issue #698's fix: glomming digit-bearing +# names must NOT change how @RULES literals tokenize. +# NOTE: "covid19" must stay TWO rule elements ("covid" then "19") so it +# keeps matching the two tokens the text tokenizer produces, and an +# escaped "\(" must not trigger the function-name glom. +############################################### + +@NODES _ROOT + +@RULES +_COVID <- covid19 @@ + +@RULES +_APAREN <- a1 \( @@ diff --git a/data/rfb/spec/bigtok.nlp b/data/rfb/spec/bigtok.nlp index 9771ced3..329920cd 100644 --- a/data/rfb/spec/bigtok.nlp +++ b/data/rfb/spec/bigtok.nlp @@ -101,6 +101,28 @@ _NONLIT [base] <- \_ _xWILD [s plus match=(_xALPHA _xEMOJI _xNUM)] @@ # 06/11/26 @RULES _ARROW [base] <- \< \- @@ +# FUNCTION NAMES CONTAINING DIGITS OR UNDERSCORES. # 07/26/26 DD. +# The rule-file tokenizer splits "Round2" into "Round" + "2", so a user +# function named Round2 (or Clamp01, Round_2, Fixed2) never matched the +# _LIT \( function grammar and died with a misleading "Missing ';'". +# Glom the adjacent alpha/numeric/underscore tokens back into one _LIT, +# but ONLY when they are immediately followed by an UNESCAPED "(" -- ie, a +# function definition header or a function call. @RULES literals write +# "\(" and key=(val) pairs put "=" before the paren, so element literals +# and pair values keep their old tokenization. +# The second element is [plus], so single-token names (if, while, return, +# L, N, X, G, P, s) never come here; they fall through to the keyword +# rules below. +@POST + rfaname(1,2) + singler(1,2) +@RULES +_LIT [base] <- + _xALPHA + _xWILD [s plus match=(_xALPHA _xNUM \_)] + \( [lookahead] + @@ + @POST rfaname(1) single() diff --git a/lite/postrfa.cpp b/lite/postrfa.cpp index e9e57c7a..94316ad7 100644 --- a/lite/postrfa.cpp +++ b/lite/postrfa.cpp @@ -141,7 +141,9 @@ if (count_) * SUBJ: Create a semantic object to hold node's proper name. * RET: True if ok, else false. * RULE: _LIT <- _xALPHA @@ -* FORM: postRFAname("1") -- arg is for the rhs token. +* _LIT <- _xALPHA _xWILD [s plus match=(_xALPHA _xNUM \_)] \( [lookahead] @@ +* FORM: postRFAname("1") -- arg is for the rhs token. +* postRFAname("1","2") -- name spans the given range of collects. ********************************************/ bool PostRFA::postRFAname( @@ -165,6 +167,56 @@ if (nlppp->sem_) return false; } +// TWO-ARG FORM. The name spans a range of collects, because the rule-file +// tokenizer split it into several adjacent tokens (eg, Round2 => "Round" "2", +// Round_2 => "Round" "_" "2"). Glom their covered text back together. +// // 07/26/26 DD. +if (args->Right()) + { + Node *nfirst, *nlast; + nfirst = nlast = 0; + if (!args_range(_T("name"), args, nlppp->collect_, nlppp->sem_, nlppp, + /*DU*/ nfirst, nlast)) + return false; + if (!nfirst || !nlast) + return false; + + if (Verbose()) + *gout << _T(" [Executing RFA name action (range).]") << std::endl; + + _TCHAR buf[MAXSTR]; // Build string in here. + _TCHAR *ptr; + long count; + count = MAXSTR; // Track space left in buffer. + buf[0] = '\0'; + ptr = &(buf[0]); // Track first empty loc in buffer. + + // Traverse the matched tokens, gathering their covered text. + Node *nelt, *bound; + bound = nlast->Right(); // One past the last matched token. + Pn *pn; + _TCHAR *text; + long len; + for (nelt = nfirst; nelt && nelt != bound; nelt = nelt->Right()) + { + pn = nelt->getData(); + text = pn->getText(); + len = pn->getEnd() - pn->getStart() + 1; + if (!strncat_e(ptr, text, len, count)) + return false; + } + + Sym *sym; + sym = nlppp->parse_->getSym(buf); + assert(sym); + + RFASem *rangesem; + rangesem = new RFASem(sym->getStr()); + nlppp->sem_ = rangesem; + + return true; + } + Iarg *arg1; arg1 = args->getData(); if ((args = args->Right())) From a87b50100289b59dcb5998e43070c28695107157 Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 26 Jul 2026 09:28:53 -0400 Subject: [PATCH 4/4] ci: don't fail the #698 step when the engine writes no make_ana.log The Linux jobs went red on the new step even though the analyzer built fine ("[Loaded analyzer: 0.005928 sec]", no errors). Windows always leaves a logs/make_ana.log behind, but Linux only writes one when the build had something to report, so an unconditional `cat` hit "No such file or directory" and `set -e` killed the step. Print the log only when it exists, and add an explicit "no out.txt -- the analyzer did not build" guard so a genuine build failure still fails loudly instead of tripping over a missing file. Verified both ways locally: green with the fix (with and without a logs dir), and red against the pre-fix grammar. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-linux.yml | 12 ++++++++++-- .github/workflows/build-windows.yml | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index aaab1985..20999e1e 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -149,9 +149,17 @@ jobs: FIX=.github/workflows/tests/fnname-digits rm -rf "$FIX/logs" "$FIX/input/text.txt_log" ./bin/nlp -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" - cat "$FIX/logs/make_ana.log" + # Windows always leaves a make_ana.log behind; Linux only writes one + # when the build had something to say. Print it if it's there. + if [ -f "$FIX/logs/make_ana.log" ]; then cat "$FIX/logs/make_ana.log"; fi + OUT="$FIX/input/text.txt_log/out.txt" TREE="$FIX/input/text.txt_log/final.tree" - got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + # nlp exits 0 even when the analyzer fails to build, so assert the + # output, not the exit code. + if [ ! -f "$OUT" ]; then + echo "FAIL(#698): no out.txt -- the analyzer did not build"; exit 1 + fi + got=$(tr -d '\r' < "$OUT") if [ "$got" != "3 21 201 2001 1 8 71" ]; then echo "FAIL(#698): out.txt was '$got'"; exit 1 fi diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 6e1be8ee..d84e1789 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -74,9 +74,17 @@ jobs: FIX=.github/workflows/tests/fnname-digits rm -rf "$FIX/logs" "$FIX/input/text.txt_log" ./bin/Release/nlp.exe -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" - cat "$FIX/logs/make_ana.log" + # Windows always leaves a make_ana.log behind; Linux only writes one + # when the build had something to say. Print it if it's there. + if [ -f "$FIX/logs/make_ana.log" ]; then cat "$FIX/logs/make_ana.log"; fi + OUT="$FIX/input/text.txt_log/out.txt" TREE="$FIX/input/text.txt_log/final.tree" - got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + # nlp exits 0 even when the analyzer fails to build, so assert the + # output, not the exit code. + if [ ! -f "$OUT" ]; then + echo "FAIL(#698): no out.txt -- the analyzer did not build"; exit 1 + fi + got=$(tr -d '\r' < "$OUT") if [ "$got" != "3 21 201 2001 1 8 71" ]; then echo "FAIL(#698): out.txt was '$got'"; exit 1 fi