diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0e041d03..deae83c1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -1079,6 +1079,40 @@ jobs: fi echo "[literal] makeliteral() makes a token later passes match" + - name: attr="value" on rule elements (#438) (Non-20.04) + if: matrix.id != 'docker-ubuntu-20.04' + shell: bash + run: | + set -eo pipefail + # A key on a rule element that is not a modifier, with one value, is + # an attribute test (#438): _xALPHA [pos="noun"] is vareq=("pos" "noun"). + # Digits compare as a number, as do the values of vareq=/vargt=/... + # written on an element. A key one edit from a modifier warns. + FIX=.github/workflows/tests/element-eq + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + mkdir -p "$FIX/logs" + ./bin/nlp -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" + OUT="$FIX/input/text.txt_log/output.txt" + if [ ! -f "$OUT" ]; then + echo "FAIL(element-eq): no output.txt -- the analyzer did not build"; exit 1 + fi + got=$(tr -d '\r' < "$OUT") + want=$(printf '%s\n' \ + 'text attr=value: two' \ + 'text vareq=: two' \ + 'number attr=value: three' \ + 'number vareq=: three' \ + 'both attr=value: two' \ + 'both vareq=: two' \ + 'greater: three') + if [ "$got" != "$want" ]; then + echo "FAIL(element-eq): output.txt was:"; echo "$got"; echo "wanted:"; echo "$want"; exit 1 + fi + if ! grep -q "Element key=mtch is not a modifier.*Did you mean match" "$FIX/logs/make_ana.log"; then + echo "FAIL(element-eq): no typo warning for mtch= in make_ana.log"; cat "$FIX/logs/make_ana.log"; exit 1 + fi + echo "[element-eq] attr=value on an element tests the attribute" + - name: factorial() up to 20! (Non-20.04) if: matrix.id != 'docker-ubuntu-20.04' shell: bash diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 899e16e7..862e0f70 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -975,6 +975,39 @@ jobs: fi echo "[literal] makeliteral() makes a token later passes match" + - name: attr="value" on rule elements (#438) + shell: bash + run: | + set -eo pipefail + # A key on a rule element that is not a modifier, with one value, is + # an attribute test (#438): _xALPHA [pos="noun"] is vareq=("pos" "noun"). + # Digits compare as a number, as do the values of vareq=/vargt=/... + # written on an element. A key one edit from a modifier warns. + FIX=.github/workflows/tests/element-eq + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + mkdir -p "$FIX/logs" + ./bin/Release/nlp.exe -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" + OUT="$FIX/input/text.txt_log/output.txt" + if [ ! -f "$OUT" ]; then + echo "FAIL(element-eq): no output.txt -- the analyzer did not build"; exit 1 + fi + got=$(tr -d '\r' < "$OUT") + want=$(printf '%s\n' \ + 'text attr=value: two' \ + 'text vareq=: two' \ + 'number attr=value: three' \ + 'number vareq=: three' \ + 'both attr=value: two' \ + 'both vareq=: two' \ + 'greater: three') + if [ "$got" != "$want" ]; then + echo "FAIL(element-eq): output.txt was:"; echo "$got"; echo "wanted:"; echo "$want"; exit 1 + fi + if ! grep -q "Element key=mtch is not a modifier.*Did you mean match" "$FIX/logs/make_ana.log"; then + echo "FAIL(element-eq): no typo warning for mtch= in make_ana.log"; cat "$FIX/logs/make_ana.log"; exit 1 + fi + echo "[element-eq] attr=value on an element tests the attribute" + - name: factorial() up to 20! shell: bash run: | diff --git a/.github/workflows/tests/element-eq/input/text.txt b/.github/workflows/tests/element-eq/input/text.txt new file mode 100644 index 00000000..3750c6e1 --- /dev/null +++ b/.github/workflows/tests/element-eq/input/text.txt @@ -0,0 +1 @@ +one two three diff --git a/.github/workflows/tests/element-eq/spec/analyzer.seq b/.github/workflows/tests/element-eq/spec/analyzer.seq new file mode 100644 index 00000000..82fafacb --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/analyzer.seq @@ -0,0 +1,10 @@ +tokenize nil # +nlp mark # +nlp text_eq # +nlp text_vareq # +nlp number_eq # +nlp number_vareq # +nlp both_eq # +nlp both_vareq # +nlp greater # +nlp typo # diff --git a/.github/workflows/tests/element-eq/spec/both_eq.nlp b/.github/workflows/tests/element-eq/spec/both_eq.nlp new file mode 100644 index 00000000..d214cfe3 --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/both_eq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: both_eq.nlp +# SUBJ: An attribute test beside a match list and a repeat. (attr=value form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "both attr=value: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xWILD [one match=(one two) pos="noun"] @@ diff --git a/.github/workflows/tests/element-eq/spec/both_vareq.nlp b/.github/workflows/tests/element-eq/spec/both_vareq.nlp new file mode 100644 index 00000000..d74dae7d --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/both_vareq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: both_vareq.nlp +# SUBJ: An attribute test beside a match list and a repeat. (vareq= form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "both vareq=: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xWILD [one match=(one two) vareq=("pos" "noun")] @@ diff --git a/.github/workflows/tests/element-eq/spec/greater.nlp b/.github/workflows/tests/element-eq/spec/greater.nlp new file mode 100644 index 00000000..c46d2266 --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/greater.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: greater.nlp +# SUBJ: vargt= on an element compares with the number 2, not the text "2". +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "greater: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [vargt=("count" 2)] @@ diff --git a/.github/workflows/tests/element-eq/spec/mark.nlp b/.github/workflows/tests/element-eq/spec/mark.nlp new file mode 100644 index 00000000..709ab07d --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/mark.nlp @@ -0,0 +1,20 @@ +############################################### +# FILE: mark.nlp +# SUBJ: Gives "two" pos=noun and "three" count=3. +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + N("pos",1) = "noun"; + noop(); +@RULES +_xNIL <- two @@ + +@POST + N("count",1) = 3; + noop(); +@RULES +_xNIL <- three @@ diff --git a/.github/workflows/tests/element-eq/spec/number_eq.nlp b/.github/workflows/tests/element-eq/spec/number_eq.nlp new file mode 100644 index 00000000..1939202c --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/number_eq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: number_eq.nlp +# SUBJ: A number attribute; digits compare as a number. (attr=value form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "number attr=value: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [count=3] @@ diff --git a/.github/workflows/tests/element-eq/spec/number_vareq.nlp b/.github/workflows/tests/element-eq/spec/number_vareq.nlp new file mode 100644 index 00000000..ea187f6d --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/number_vareq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: number_vareq.nlp +# SUBJ: A number attribute; digits compare as a number. (vareq= form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "number vareq=: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [vareq=("count" 3)] @@ diff --git a/.github/workflows/tests/element-eq/spec/text_eq.nlp b/.github/workflows/tests/element-eq/spec/text_eq.nlp new file mode 100644 index 00000000..43d2139b --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/text_eq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: text_eq.nlp +# SUBJ: A text attribute. (attr=value form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "text attr=value: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [pos="noun"] @@ diff --git a/.github/workflows/tests/element-eq/spec/text_vareq.nlp b/.github/workflows/tests/element-eq/spec/text_vareq.nlp new file mode 100644 index 00000000..4142aa43 --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/text_vareq.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: text_vareq.nlp +# SUBJ: A text attribute. (vareq= form) +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "text vareq=: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [vareq=("pos" "noun")] @@ diff --git a/.github/workflows/tests/element-eq/spec/typo.nlp b/.github/workflows/tests/element-eq/spec/typo.nlp new file mode 100644 index 00000000..2a5fafbb --- /dev/null +++ b/.github/workflows/tests/element-eq/spec/typo.nlp @@ -0,0 +1,14 @@ +############################################### +# FILE: typo.nlp +# SUBJ: mtch is one edit from match: warns, then tests an attribute "mtch". +# NOTE: Part of the element-eq regression (#438); expected output +# is in the CI step. +############################################### + +@NODES _ROOT + +@POST + "output.txt" << "typo: " << phrasetext() << "\n"; + noop(); +@RULES +_xNIL <- _xALPHA [mtch=(two)] @@ diff --git a/lite/ielt.cpp b/lite/ielt.cpp index e3e03a76..c963f9d2 100644 --- a/lite/ielt.cpp +++ b/lite/ielt.cpp @@ -1354,11 +1354,23 @@ for (dpair = list->getFirst(); dpair; dpair = dpair->Right()) Dlist *args = vals ? Iarg::copy_args(vals, parse, true) : 0; // Element values are strings; the actions that take numbers get // numbers, as they do from a @PRE region (the compiled form needs it). - if (args && (!strcmp_i(key, _T("numrange")) - || !strcmp_i(key, _T("length")) - || !strcmp_i(key, _T("lengthr")))) + // For the var comparisons only the value (second argument) can be a + // number: vargt=("count" 2) must compare with 2, not "2". + Delt *from = 0; + if (!args) + ; + else if (!strcmp_i(key, _T("numrange")) + || !strcmp_i(key, _T("length")) + || !strcmp_i(key, _T("lengthr"))) + from = args->getFirst(); + else if (!strcmp_i(key, _T("vareq")) + || !strcmp_i(key, _T("varne")) + || !strcmp_i(key, _T("vargt")) + || !strcmp_i(key, _T("varlt"))) + from = args->getFirst() ? args->getFirst()->Right() : 0; + if (from) { - for (Delt *darg = args->getFirst(); darg; darg = darg->Right()) + for (Delt *darg = from; darg; darg = darg->Right()) { Iarg *arg = darg->getData(); long long num = 0; @@ -1374,6 +1386,40 @@ for (dpair = list->getFirst(); dpair; dpair = dpair->Right()) elt->pres_ = new Dlist(); elt->pres_->rpush(new Ipre(name, args, ord, ord)); } + else if (vals && vals->getFirst() && !vals->getFirst()->Right()) + { + // attr="value" on the element is an attribute test (#438): + // _xALPHA [pos="noun"] is vareq=("pos" "noun") is vareq("pos","noun"); + // A value of digits only compares as a number. A misspelled modifier + // would land here too, so warn when the key is one edit from one. + const _TCHAR *similar = nearModifier(key); + if (similar) + { + std::_t_strstream gerrStr; + gerrStr << _T("[Element key=") << key << _T(" is not a modifier; testing it as an attribute. Did you mean ") + << similar << _T("?]") << std::ends; + parse->errOut(&gerrStr,false,true); + } + _TCHAR *name = 0, *attr = 0; + parse->internStr(_T("vareq"), /*UP*/ name); + parse->internStr(key, /*UP*/ attr); + Dlist *args = new Dlist(); + args->rpush(new Iarg(attr)); + Iarg *val = vals->getFirst()->getData(); + long long num = 0; + _TCHAR *vstr = (val->getType() == IASTR) ? val->getStr() : 0; + bool digits = vstr && *vstr; + for (_TCHAR *p = vstr; digits && *p; ++p) + if (*p < '0' || *p > '9') + digits = false; + if (digits && str_to_long(vstr, num)) + args->rpush(new Iarg(num)); + else + args->rpush(Iarg::copy_arg(val)); + if (!elt->pres_) + elt->pres_ = new Dlist(); + elt->pres_->rpush(new Ipre(name, args, ord, ord)); + } else { std::_t_strstream gerrStr; @@ -1422,6 +1468,54 @@ else if (elt->max_ == -1) } +/******************************************** +* FN: NEARMODIFIER +* SUBJ: The element modifier KEY is one edit away from, if any (#438). +* NOTE: An unknown key with a value is an attribute test, so a typo such as +* mtch=(...) would silently test an attribute named "mtch". Used to warn. +********************************************/ + +static bool oneEdit(const _TCHAR *a, const _TCHAR *b) +{ +size_t la = _tcslen(a), lb = _tcslen(b); +if (la > lb + 1 || lb > la + 1) + return false; +size_t i = 0, j = 0; +int edits = 0; +while (i < la && j < lb) + { + if (_totlower(a[i]) == _totlower(b[j])) + { ++i; ++j; continue; } + if (++edits > 1) + return false; + if (la > lb) ++i; + else if (lb > la) ++j; + else { ++i; ++j; } + } +edits += (int) ((la - i) + (lb - j)); +return edits == 1; +} + +const _TCHAR *Ielt::nearModifier(_TCHAR *key) +{ +static const _TCHAR *mods[] = { + _T("attr"), _T("attrs"), _T("base"), _T("deaccent"), _T("dejunk"), _T("except"), + _T("excepts"), _T("fail"), _T("fails"), _T("group"), _T("layer"), _T("layers"), + _T("look"), _T("lookahead"), _T("match"), _T("matches"), _T("max"), _T("min"), + _T("nest"), _T("one"), _T("opt"), _T("option"), _T("optional"), _T("pass"), + _T("passes"), _T("plus"), _T("recurse"), _T("rename"), _T("singlet"), _T("star"), + _T("tree"), _T("trig"), _T("trigger"), _T("unsealed"), _T("xsinglet"), _T("xtree"), + 0 + }; +if (!key || _tcslen(key) < 3) + return 0; // Too short to tell a typo from a real attribute name. +for (const _TCHAR **p = mods; *p; ++p) + if (oneEdit(key, *p)) + return *p; +return 0; +} + + /******************************************** * FN: PREACTIONKEY * SUBJ: Whether an element key names a @PRE action (#438). diff --git a/lite/ielt.h b/lite/ielt.h index 6b04ee47..ed91b9f6 100644 --- a/lite/ielt.h +++ b/lite/ielt.h @@ -131,6 +131,7 @@ class Ielt : public Ielement /*UP*/ bool &info, bool &f_look, bool fhash = true); static bool preActionKey(_TCHAR *key); // #438. + static const _TCHAR *nearModifier(_TCHAR *key); // #438. void internPres( // 07/13/99 AM. Dlist *pres, // For binding pre actions. int ord // Current element number in rule's phrase. diff --git a/nlp/main.cpp b/nlp/main.cpp index d883604f..26e60aaf 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 "4.2.28" +#define NLP_ENGINE_VERSION "4.2.29" bool cmdReadArgs(int, _TCHAR *argv[], _TCHAR *&, _TCHAR *&, _TCHAR *&, _TCHAR *&, bool &, bool &, bool &, bool &, bool &, bool &, int &, bool &); void cmdHelpargs(_TCHAR *);