Skip to content

Added support for terminal comma#1

Open
hoodmane wants to merge 5 commits into
Erlkoenig90:masterfrom
hoodmane:handle-terminal-comma
Open

Added support for terminal comma#1
hoodmane wants to merge 5 commits into
Erlkoenig90:masterfrom
hoodmane:handle-terminal-comma

Conversation

@hoodmane

@hoodmane hoodmane commented Jan 8, 2021

Copy link
Copy Markdown

Terminal commas are nice for defining a macro like

DO_STUFF(
   thing1,
   thing2,
   thing3,
);

but currently MAP(f,a,b,c,) invokes f() one extra time. Sometimes it is impractical to modify f to support this, so I defined MAP_ALLOW_TRAILING_COMMA that skips empty arguments.

These macros allow the user to pass a single value to all invocations of the user macro,
and the *_I variants additionally pass the index of the invocation as an integer literal.
This requires a list of predefined increment operations. Updated the documentation
accordingly, and added an overview and a link to the µSer library to README.md.
@hoodmane hoodmane force-pushed the handle-terminal-comma branch from 5c5064e to 847c4ba Compare January 8, 2021 20:04
@hoodmane hoodmane force-pushed the handle-terminal-comma branch from 847c4ba to 45033b3 Compare January 8, 2021 20:05
@Erlkoenig90

Copy link
Copy Markdown
Owner

Thanks for the suggestion! I'll take a closer look later. One thing I noticed: C forbids identifiers starting with _+uppercase letter, so _TRIGGER_PARENTHESIS_ is a reserved identifier. Also, it would be useful if all macro names in this library start with MAP_, as macros are not scoped. Could you change the names appropriately?

@hoodmane

hoodmane commented Jan 8, 2021

Copy link
Copy Markdown
Author

Will do, thanks. I wanted to see whether this fork was actively maintained before getting into it too much. I think ideally it would be good to make similar variants of the other MAP variants. Also, this doesn't just behave correctly with a terminal comma, it ignores ALL empty arguments , so maybe there is need to clearly document that difference in behavior.

@hoodmane

hoodmane commented Jan 8, 2021

Copy link
Copy Markdown
Author

I come from a LaTeX macro programming background and only recently made a foray into C macros so I don't know the conventions.

I think ideally there would be a distinct convention for private implementation details macros and public entrypoints. I think the standard says that underscore followed by a lowercase letter is not reserved, so one could consider naming public macros with the prefix MAP_ and private ones with the prefix _map_, but this is a separate issue from this PR.

@hoodmane hoodmane force-pushed the handle-terminal-comma branch from a58dcb7 to c1eee36 Compare January 8, 2021 23:19
@Erlkoenig90

Copy link
Copy Markdown
Owner

For C it's convention to only use uppercase letters in macros, to distinguish them from other identifiers, because macros have global scope. Maybe it would be a good idea to not provide macros like MAP_HANDLE_TRAILING_COMMA, but just MAP_DO_F_IF_NONEMPTY, so the user can do the check inside the map function. That way we could avoid having a HANDLE_TRAILING_COMMA variant of every MAP macro. Unfortunately I don't really understand how it works - does it work with GCC, Clang and MSVC?

@hoodmane

Copy link
Copy Markdown
Author

For C it's convention to only use uppercase letters in macros, to distinguish them from other identifiers, because macros have global scope.

My primary experience is with CPython: CPython uses Py_ALL_CAPS or PySubModule_ALL_CAPS (e.g., Py_CLEAR) so that they can scope the macro to the module using the same prefix as the rest of the functions from the module. Then they use _Py_NAME to mean that the macro hasn't been completely standardized yet and is likely to change in future versions. I guess these decisions are a bit idiosyncratic.

Maybe it would be a good idea to not provide macros like MAP_HANDLE_TRAILING_COMMA, but just MAP_DO_F_IF_NONEMPTY

Yes, that sounds like a good choice to me.

does it work with GCC, Clang and MSVC?

I was running it with gcc. I just tested it with clang and it works as expected. I'm not really sure how to install msvc does it work in linux? apt doesn't suggest anything.

@hoodmane

hoodmane commented Jan 10, 2021

Copy link
Copy Markdown
Author

Unfortunately I don't really understand how it works

Let me explain a bit. See also: https://gustedt.wordpress.com/2010/06/08/detect-empty-macro-arguments/ where I took the approach from.

MAP_HAS_COMMA

This cannot tell the difference between an empty argument and a nonempty one, but it can tell the difference between 0 or 1 argument (no comma) and 2 arguments (yes comma). It fails on 3 or more arguments (but can easily be extended to handle any fixed number of arguments).

With no comma:

MAP_HAS_COMMA(xxxx) => MAP_HAS_COMMA_HELPER(xxx, 1, 0)
MAP_HAS_COMMA_HELPER(_0 = xxx, _1 = 1, _2 = 0, __VAR_ARGS__ = <empty>) ==> _2 = 0

so final result is 0. Note that it works just the same if the argument is completely empty: then the helper arguments are _0=<empty>, _1=1, _2=0, __VAR_ARGS_=<empty>.

With a comma:

MAP_HAS_COMMA(xxxx, yyy) => MAP_HAS_COMMA_HELPER(xxxx, yyy, 1, 0)
MAP_HAS_COMMA_HELPER(_0 = xxx, _1 = yyy, _2 = 1, __VAR_ARGS__ = 0 ) ==> _2 = 1

so final result is 1.

MAP_IS_EMPTY

Test whether argument is empty. If it's empty, expand to 1 else expand to 0. Should only be given a single argument (meaning no commas).

The idea here is to convert an empty argument list into one with a comma, and a nonempty one with a single argument into a comma. The key idea is this:

#define TEST() 1
TEST <nonempty> () // TEST can't be expanded, it's been separated from the argument list
TEST <empty> () // Looks like TEST(), expands to 1

(This is the same approach used with the DEFER macro which enables recursion with macros.)
So:

#define MAP_IS_EMPTY(args...) MAP_HAS_COMMA(MAP__TRIGGER_PARENTHESIS__ args())
#define MAP__TRIGGER_PARENTHESIS__(...) ,

MAP_IS_EMPTY(stuff) ==> MAP_HAS_COMMA(MAP__TRIGGER_PARENTHESIS__ stuff ()) ==> ... ==> no comma so 0.
MAP_IS_EMPTY() ==> MAP_HAS_COMMA(MAP__TRIGGER_PARENTHESIS__ ()) ==>MAP_HAS_COMMA(,) ==> ... ==> yes comma so 1.

MAP_TEST_PREDICATE

If test expands to 1, expand case_true, otherwise expand case_false.
Idea here is quite simple: MAP_TEST_PREDICATE_0 takes first of two arguments, MAP_TEST_PREDICATE_1 takes second of two arguments, we choose between them with ##. However, ## blocks expansion of the argument test which we want so we add an extra layer of indirection to allow the argument to be expanded.

#define MAP_TEST_PREDICATE(test, case_true, case_false)                                  
  MAP_TEST_PREDICATE_EXP(test, case_true, case_false)
#define MAP_TEST_PREDICATE_EXP(test, case_true, case_false)                              
  MAP_TEST_PREDICATE_##test(case_true, case_false)
#define MAP_TEST_PREDICATE_0(case_true, case_false) case_false
#define MAP_TEST_PREDICATE_1(case_true, case_false) case_true

The remaining two macros MAP_IF_EMPTY and MAP_DO_F_IF_NONEMPTY are simple specializations of MAP_TEST_PREDICATE and MAP_IS_EMPTY

@Erlkoenig90

Copy link
Copy Markdown
Owner

Thanks for the detailed descriptions, that clears it up.

I guess these decisions are a bit idiosyncratic.

C has its own idiosyncrasies :) Here we do macros in all caps. To avoid all collisions with variables/functions, different naming schemes are used.

I was running it with gcc. I just tested it with clang and it works as expected. I'm not really sure how to install msvc does it work in linux? apt doesn't suggest anything.

MSVC is the Microsoft Visual C++ compiler, it doesn't run on Linux. I implemented a simple automated test for the macros and managed to set up a Github Action that will automatically compile + test the code on a GitHub server on Linux, Windows and Mac using GCC, MSVC and Clang, respectively. If you merge my master into yours and implement similar tests, you'll get automatic testing from those compilers as well.

@hoodmane

Copy link
Copy Markdown
Author

Thanks, will do. Do you want me to put my tests in gentest.py as well?

@Erlkoenig90

Copy link
Copy Markdown
Owner

Yes please... I used a code generator to produce long test cases to make sure it works with 365 arguments. That's probably not necessary with your part, so just add it at the top of the file to the "static" portion of C code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants