Skip to content

Commit d1fffd6

Browse files
committed
toke.c: S_intuit_more: Add more commentary
This function is described in its comments as 'terrifying', and by its original author, Larry Wall, as "truly awful". As a result, it has been mostly untouched since its introduction in 1993. That means it has not been updated as new language features have been added. As an example, it does not know about lexical variables, so the code it has for globals just doesn't work on the vast majority of modern day coding practices. And it is buggy. A few years ago, I set out to try to understand it. I added commentary and simplified some overly complicated expressions, but left its behavior unchanged. Now, I set out to make some changes, and found many more issues than I had earlier. This commit adds commentary about those. Hopefully this will lead to some discussion and a consensus on the way forward.
1 parent 040a4d7 commit d1fffd6

File tree

1 file changed

+151
-17
lines changed

1 file changed

+151
-17
lines changed

toke.c

Lines changed: 151 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4545,19 +4545,47 @@ S_intuit_more(pTHX_ char *s, char *e)
45454545
/* Here is '[': maybe we have a character class. Examine the guts */
45464546
s++;
45474547

4548+
/* khw: If the context of this call is $foo[...], we may be able to avoid
4549+
* the heuristics below. The possibilities are:
4550+
* strict @foo $foo
4551+
* vars? exists exists
4552+
* y n n This is an error; return false now
4553+
* y n y must be a a charclass
4554+
* y y n must be a a subscript
4555+
* y y y ambiguous; do heuristics below
4556+
* n n n ambiguous; do heuristics below
4557+
* n n y ambiguous; do heuristics below, but I
4558+
* wonder if the initial bias should be a
4559+
* little towards charclass
4560+
* n y n ambiguous; do heuristics below, but I
4561+
* wonder if the initial bias should be a
4562+
* little towards subscript
4563+
* n y y ambiguous; do heuristics below
4564+
*/
4565+
45484566
/* '^' implies a character class; An empty '[]' isn't legal, but it does
45494567
* mean there isn't more to come */
45504568
if (s[0] == ']' || s[0] == '^')
45514569
return FALSE;
45524570

4553-
/* Find matching ']'. khw: This means any s[1] below is guaranteed to
4554-
* exist */
4571+
/* Find matching ']'. khw: Actually it finds the next ']' and assumes it
4572+
* matches the '['. In order to account for the possibility of the ']'
4573+
* being inside the scope of \Q or preceded by an even number of
4574+
* backslashes, this should be rewritten */
45554575
const char * const send = (char *) memchr(s, ']', e - s);
45564576
if (! send) /* has to be an expression */
45574577
return TRUE;
45584578

4579+
/* Below here, the heuristics start. One idea from alh is, given 'use
4580+
* 5.43.x', that for all digits, that if we have to resort to heuristics,
4581+
* we instead raise an error with an explanation of how to make it
4582+
* unambiguous: ${foo}[123] */
4583+
45594584
/* If the construct consists entirely of one or two digits, call it a
4560-
* subscript. */
4585+
* subscript.
4586+
*
4587+
* khw: No one writes 03 to mean 3. Any string of digits beginning with
4588+
* '0' is likely to be a charclass, including length 2 ones. */
45614589
if (isDIGIT(s[0]) && send - s <= 2 && (send - s == 1 || (isDIGIT(s[1])))) {
45624590
return TRUE;
45634591
}
@@ -4613,15 +4641,42 @@ S_intuit_more(pTHX_ char *s, char *e)
46134641
char tmpbuf[sizeof PL_tokenbuf * 4];
46144642
scan_ident(s, tmpbuf, sizeof tmpbuf, FALSE);
46154643
len = (int)strlen(tmpbuf);
4644+
4645+
/* khw: This only looks at global variables; lexicals came
4646+
* later, and this hasn't been updated. Ouch!! */
46164647
if ( len > 1
46174648
&& gv_fetchpvn_flags(tmpbuf,
46184649
len,
46194650
UTF ? SVf_UTF8 : 0,
46204651
SVt_PV))
4652+
{
46214653
weight -= 100;
4622-
else /* Not a multi-char identifier already known in the
4623-
program; is somewhat likely to be a subscript */
4654+
4655+
/* khw: Below we keep track of repeated characters; People
4656+
* rarely say qr/[aba]/, as the second a is pointless.
4657+
* (Some do it though as a mnemonic that is meaningful to
4658+
* them.) But generally, repeated characters makes things
4659+
* more likely to be a charclass. But we have here that
4660+
* this an identifier so likely a subscript. Its spelling
4661+
* should be irrelevant to the repeated characters test.
4662+
* So, we should advance past it. Suppose it is a hash
4663+
* element, like $subscripts{$which}. We should advance
4664+
* past the braces and key */
4665+
}
4666+
else {
4667+
/* Not a multi-char identifier already known in the
4668+
* program; is somewhat likely to be a subscript.
4669+
*
4670+
* khw: Our test suite contains several constructs like
4671+
* [$A-Z]. Excluding length 1 identifiers in the
4672+
* conditional above means such are much less likely to be
4673+
* mistaken for subscripts. I would argue that if the next
4674+
* character is a '-' followed by an alpha, that would make
4675+
* it much more likely to be a charclass. It would only
4676+
* make sense to be an expression if that alpha string is a
4677+
* bareword with meaning; something like [$A-ord] */
46244678
weight -= 10;
4679+
}
46254680
}
46264681
else if ( s[0] == '$'
46274682
&& s[1]
@@ -4638,13 +4693,43 @@ S_intuit_more(pTHX_ char *s, char *e)
46384693
}
46394694
break;
46404695

4696+
/* khw: [:blank:] strongly indicates a charclass */
4697+
46414698
case '\\':
46424699
if (s[1]) {
4643-
if (memCHRs("wds]", s[1]))
4700+
if (memCHRs("wds]", s[1])) {
46444701
weight += 100; /* \w \d \s => strongly charclass */
4645-
/* khw: Why not \W \D \S \h \v, etc as well? */
4646-
else if (seen[(U8)'\''] || seen[(U8)'"'])
4647-
weight += 1; /* \' => mildly charclass */
4702+
/* khw: \] can't happen, as any ']' is beyond our search.
4703+
* Why not \W \D \S \h \v, etc as well? Should they have
4704+
* the same weights as \w \d \s or should all or some be
4705+
* in the 'abcfnrtvx' below? */
4706+
} else if (seen[(U8)'\''] || seen[(U8)'"']) {
4707+
weight += 1;
4708+
/* khw: This is problematic. Enough so, that I misread
4709+
* it, and added a wrong comment about what it does in
4710+
* 57ae1f3a8e669082e3d5ec6a8cdffbdc39d87bee. Note that it
4711+
* doesn't look at the current character. What it
4712+
* actually does is: if any quote has been seen in the
4713+
* parse, don't do the rest of the else's below, but for
4714+
* every subsequent backslashed character encountered
4715+
* (except \0 \w \s \d), increment the weight to lean a
4716+
* bit more towards being a charclass. That means that
4717+
* every backslash sequence following the first occurrence
4718+
* of a quote increments the weight regardless of what the
4719+
* sequence is. Again, \0 \w \d and \s are not controlled
4720+
* by this else, so they change the weight by a lot more.
4721+
* But what makes them so special that they aren't subject
4722+
* to this. Any why does having a quote change the
4723+
* behavior from then on. And why only backslashed
4724+
* sequences get this treatment? This code has been
4725+
* unchanged since this function was added in 1993. I
4726+
* don't get it. Instead, it does seem to me that it is
4727+
* especially unlikely to repeat a quote in a charclass,
4728+
* but that having just a single quote is indicative of a
4729+
* charclass, and having pairs of quotes is indicative of
4730+
* a subscript. Similarly for things that could indicate
4731+
* nesting of braces or parens. */
4732+
}
46484733
else if (memCHRs("abcfnrtvx", s[1]))
46494734
weight += 40; /* \n, etc => charclass */
46504735
/* khw: Why not \e etc as well? */
@@ -4653,6 +4738,19 @@ S_intuit_more(pTHX_ char *s, char *e)
46534738
while (s[1] && isDIGIT(s[1]))
46544739
s++;
46554740
}
4741+
4742+
/* khw: There are lots more possible escape sequences. Some,
4743+
* like \A,\z have no special meaning to charclasses, so might
4744+
* indicate a subscript, but I don't know what they would be
4745+
* doing there either. Some have been added to the language
4746+
* after this code was written, but no one thought to, or
4747+
* could wade through this function, to add them. Things like
4748+
* \p{} for properties, \N and \N{}, for example.
4749+
*
4750+
* It's problematic that \a is treated as plain 'a' for
4751+
* purposes of the 'seen' array. Whatever is matched by these
4752+
* backslashed sequences should not be added to 'seen'. That
4753+
* includes the backslash. */
46564754
}
46574755
else /* \ followed by NUL strongly indicates character class */
46584756
weight += 100;
@@ -4698,7 +4796,25 @@ S_intuit_more(pTHX_ char *s, char *e)
46984796
&& isALPHA(s[1]))
46994797
{
47004798
/* Here it's \W (that isn't [$@&] ) followed immediately by two
4701-
* alphas in a row. Accumulate all the consecutive alphas */
4799+
* alphas in a row. Accumulate all the consecutive alphas.
4800+
*
4801+
* khw: The code below was changed in 2015 by
4802+
* 56f81afc0f2d331537f38e6f12b86a850187cb8a to solve a
4803+
* buffer overrun. Prior to that commit, the code copied all
4804+
* the consecutive alphas to a temporary. The problem was
4805+
* that temporary's size could be exceeded, and the temporary
4806+
* wasn't even needed (at least by 2015). The called
4807+
* keyword() function doesn't need a copy. It takes a pointer
4808+
* to the first character and a length, hence it can operate
4809+
* on the original source text. It is intended to catch cases
4810+
* like $a[ord]. If it does match a keyword, we don't want
4811+
* the spelling of that keyword to affect the seen[] array.
4812+
* But if it isn't a keyword we do want to fall back to the
4813+
* normal behavior. And the 2015 commit removed that. It
4814+
* absorbs every bareword regardless, defeating the intent of
4815+
* the algorithm implementing the heuristics. That not many
4816+
* bugs have surfaced since indicates this whole thing doesn't
4817+
* get applied very much */
47024818
char *d = s;
47034819
while (isALPHA(s[0]))
47044820
s++;
@@ -4708,7 +4824,12 @@ S_intuit_more(pTHX_ char *s, char *e)
47084824
if (keyword(d, s - d, 0))
47094825
weight -= 150;
47104826

4711-
/* khw: Should those alphas be marked as seen? */
4827+
/* khw: Barewords could also be subroutine calls, and these
4828+
* would also indicate a subscript. Like [green] where
4829+
* 'green' has been declared, for example, in 'use constant'
4830+
* Or maybe it should just call intuit_method() which checks
4831+
* for keyword, subs, and methods.
4832+
* */
47124833
}
47134834

47144835
/* Consecutive chars like [...12...] and [...ab...] are presumed
@@ -4723,23 +4844,36 @@ S_intuit_more(pTHX_ char *s, char *e)
47234844
/* But repeating a character inside a character class does nothing,
47244845
* like [aba], so less likely that someone makes such a class, more
47254846
* likely that it is a subscript; the more repeats, the less
4726-
* likely. */
4847+
* likely.
4848+
*
4849+
* khw: I think this changes the weight too rapidly. Each time
4850+
* through the loop compounds the previous times. Instead, it
4851+
* would be better to have a separate loop after all the rest that
4852+
* changes the weight once based on how many times each character
4853+
* gets repeated */
47274854
weight -= seen[un_char];
47284855
break;
47294856
} /* End of switch */
47304857

47314858
/* khw: 'seen' is declared as a char. This ++ can cause it to wrap.
47324859
* This gives different results with compilers for which a plain 'char'
4733-
* is actually unsigned, versus those where it is signed. I believe it
4734-
* is undefined behavior to wrap a 'signed'. I think it should be
4735-
* instead declared an unsigned int to make the chances of wrapping
4736-
* essentially zero.
4860+
* is actually unsigned, versus those where it is signed. The C99
4861+
* standard allows a compiler to raise a signal when a 'signed' char
4862+
* is incremented outside its permissible range. I think 'seen'
4863+
* should be instead declared an unsigned, and a conditional added
4864+
* to prevent wrapping.
47374865
*
47384866
* And I believe that extra backslashes are different from other
4739-
* repeated characters. */
4867+
* repeated characters. There may be others, like I have mentioned
4868+
* quotes and paired delimiters */
47404869
seen[un_char]++;
47414870
} /* End of loop through each character of the construct */
47424871

4872+
/* khw: People on #irc have suggested things that I think boil down to:
4873+
* under 'use 5.43.x', output a warning like existing warnings for
4874+
* similar situations "Ambiguous use of [], resolved as ..." Perhaps
4875+
* suppress the message if all (or maybe almost all) the evidence points
4876+
* to the same outcome. This would involve two weight variables */
47434877
if (weight >= 0) /* probably a character class */
47444878
return FALSE;
47454879

0 commit comments

Comments
 (0)