3

Question

Is there a way to check whether a macro is fully expandable (or rather "safe in an expansion-only context" [1])?

Consider this code:

\def\a{Just a string}
\def\b{\a}
\def\c{\def\unsafe}
\def\d{\c}

How could I check which of the macros (a-d) are safe in an expansion-only context? By looking at them I know that a and b are whereas c and d are not but if I wanted to know the same for a macro I haven't written myself this could get quite useful.


Background

I am working on a way to detect whether some input is a valid number in PGF. For this I developed this approach which makes use of passing the input into \pgfmathfloatparsenumber.
The problem I have run into is that said macro appears to somehow manages to expand the input until there is an error (if the input is in fact not safe in an expansion-only-context). I tried using protected, noexpand and similar but somehow PGF manages to circumvent those.

So the idea is to check whether the input is safe before actually passing it to PGF. The problem is: I don't know how I'd go about that...

  • 4
    you can't :-)... – David Carlisle Dec 13 '18 at 8:04
  • 4
    well you can always do something of course, in the example you give it would be hard in general to avoid an undefined command error on that input, if you defined \unsafe first then the edef would give something bad but would probably not give an error during the actual edef, such cases you can probably detect. similarly if you have \edef\foo{ {\mbox} } you are going to get a low level parse error if you expand \mbox and it hits the } it woul dbe verh hard to avoid such errors if you allow bad input – David Carlisle Dec 13 '18 at 8:11
  • if you know in advance that the thing must expand say to some digits, you could try \romannumeral-`0 triggered expansion, then examine first token if a digit ok remove and repeat and do repetitively until either nothing is left or you hit some unexpandable token which is not a digit. You have to detect case of braces etc... The idea here is that \edef can cause errors if your material is not expandable, but "full-first" expansion will not. – user4686 Dec 13 '18 at 8:46
  • 1
    No, you misunderstood. I said "if the thing must expand say to some digits". If letters also are allowed then you only have to take that into account. The point is that if you know in advance what must be the full expansion outcome, then you can check it. – user4686 Dec 13 '18 at 8:50
  • 1
    @Skillmon perhaps you think of Bruno's 'unravel' – user4686 Dec 13 '18 at 15:04
5

Did I hear someone say that it's not possible?

The following defines \ifexpandable which checks whether a token is expandable (actually you can also give it a list of tokens and it checks whether all of them are expandable). I don't know whether this has any side effects. Requires LuaTeX.

\documentclass{article}

\def\ifexpandablelua{%
  \directlua{
    local t = token.scan_toks()
    local b = true
    for n,v in ipairs(t) do
        local is_assign =
            string.find(v.cmdname, "assign") \string~= nil or
            string.find(v.cmdname, "def") \string~= nil or
            string.find(v.cmdname, "let") \string~= nil or
            string.find(v.cmdname, "box") \string~= nil
        local is_call = string.find(v.cmdname, "call") \string~= nil
        print(v.cmdname, is_assign, is_call)
        b = b and (not is_assign) and (is_call and v.expandable or true)
    end
    if b then
        tex.sprint("\string\\iftrue")
    else
        tex.sprint("\string\\iffalse")
    end
  }%
}

\def\ifexpandable#1{%
  \expandafter\ifexpandablelua\expandafter{\romannumeral-`0#1}%
}

\begin{document}

\def\a{Just a string}
\def\b{\a}
\def\c{\def\unsafe}
\def\d{\c}

\edef\isexpandable{%
  \ifexpandable\section
    expandable
  \else
    not expandable
  \fi
}
\isexpandable

\end{document}
  • What about something like \def\foo{{\hbox}}\ifexpandable\foo? – Joseph Wright Dec 13 '18 at 9:03
  • @JosephWright \foo is expandable, i.e. you have to expand it once and ask \ifexpandable again. I tried to expand step-wise with token.expand but quickly realised that I actually have no idea what this function is doing. – Henri Menke Dec 13 '18 at 9:03
  • 1
    I think the OP query is not testing one token but testing an entire input which may conceivably be \foo E\bar for example. – user4686 Dec 13 '18 at 9:06
  • 1
    @Raven You could additionally check for v.cmdname. This field holds information about the token, i.e. whether it is a def or let or letter or other_char etc. – Henri Menke Dec 13 '18 at 9:29
  • 1
    @Raven I have improved my answer. It is now using the \romannumeral trick to expand everything up to the first unexpandable token. Then I feed that result into Lua to check whether there is any unexpandable call or assignment inside. – Henri Menke Dec 16 '18 at 1:21

Your Answer

By clicking "Post Your Answer", you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.