Coder Social home page Coder Social logo

hirrolot / metalang99 Goto Github PK

View Code? Open in Web Editor NEW
769.0 18.0 23.0 13.12 MB

Full-blown preprocessor metaprogramming

Home Page: https://metalang99.readthedocs.io/en/latest/

License: MIT License

Shell 0.29% C 93.41% TeX 2.94% CMake 0.37% Python 0.44% C++ 2.56%
c c99 cpp cpp11 programming-language metaprogramming macros embedded-language header-only functional-programming

metalang99's Introduction

Metalang99

CI docs book spec mailing list

The dark side of the force is a pathway to many abilities, some considered to be unnatural.
    -- Darth Sidious

Based on examples/demo.c:

Compile-time list manipulation
// 3, 3, 3, 3, 3
static int five_threes[] = {
    ML99_LIST_EVAL_COMMA_SEP(ML99_listReplicate(v(5), v(3))),
};

// 5, 4, 3, 2, 1
static int from_5_to_1[] = {
    ML99_LIST_EVAL_COMMA_SEP(ML99_listReverse(ML99_list(v(1, 2, 3, 4, 5)))),
};

// 9, 2, 5
static int lesser_than_10[] = {
    ML99_LIST_EVAL_COMMA_SEP(
        ML99_listFilter(ML99_appl(v(ML99_greater), v(10)), ML99_list(v(9, 2, 11, 13, 5)))),
};
Macro recursion
#define factorial(n)          ML99_natMatch(n, v(factorial_))
#define factorial_Z_IMPL(...) v(1)
#define factorial_S_IMPL(n)   ML99_mul(ML99_inc(v(n)), factorial(v(n)))

ML99_ASSERT_EQ(factorial(v(4)), v(24));
Overloading on a number of arguments
typedef struct {
    double width, height;
} Rect;

#define Rect_new(...) ML99_OVERLOAD(Rect_new_, __VA_ARGS__)
#define Rect_new_1(x)                                                                              \
    { x, x }
#define Rect_new_2(x, y)                                                                           \
    { x, y }

static Rect _7x8 = Rect_new(7, 8), _10x10 = Rect_new(10);

// ... and more!

int main(void) {
    // Yeah. All is done at compile time.
}

(Hint: v(something) evaluates to something.)

Metalang99 is a firm foundation for writing reliable and maintainable metaprograms in pure C99. It is implemented as an interpreted FP language atop of preprocessor macros: just #include <metalang99.h> and you are ready to go. Metalang99 features algebraic data types, pattern matching, recursion, currying, and collections; in addition, it provides means for compile-time error reporting and debugging. With our built-in syntax checker, macro errors should be perfectly comprehensible, enabling you for convenient development.

Currently, Metalang99 is used at OpenIPC as an indirect dependency of Datatype99 and Interface99; this includes an RTSP 1.0 implementation along with ~50k lines of private code.

Motivation

Macros facilitate code re-use, macros are the building material that lets you shape the language to suit the problem being solved, leading to more clean and concise code. However, metaprogramming in C is utterly castrated: we cannot even operate with control flow, integers, unbounded sequences, and compound data structures, thereby throwing a lot of hypothetically useful metaprograms out of scope.

To solve the problem, I have implemented Metalang99. Having its functionality at our disposal, it becomes possible to develop even fairly non-trivial metaprograms, such as Datatype99:

#include <datatype99.h>

datatype(
    BinaryTree,
    (Leaf, int),
    (Node, BinaryTree *, int, BinaryTree *)
);

int sum(const BinaryTree *tree) {
    match(*tree) {
        of(Leaf, x) return *x;
        of(Node, lhs, x, rhs) return sum(*lhs) + *x + sum(*rhs);
    }

    return -1;
}

Or Interface99:

#include <interface99.h>

#include <stdio.h>

#define Shape_IFACE                      \
    vfunc( int, perim, const VSelf)      \
    vfunc(void, scale, VSelf, int factor)

interface(Shape);

typedef struct {
    int a, b;
} Rectangle;

int  Rectangle_perim(const VSelf) { /* ... */ }
void Rectangle_scale(VSelf, int factor) { /* ... */ }

impl(Shape, Rectangle);

typedef struct {
    int a, b, c;
} Triangle;

int  Triangle_perim(const VSelf) { /* ... */ }
void Triangle_scale(VSelf, int factor) { /* ... */ }

impl(Shape, Triangle);

void test(Shape shape) {
    printf("perim = %d\n", VCALL(shape, perim));
    VCALL(shape, scale, 5);
    printf("perim = %d\n", VCALL(shape, perim));
}

Unlike the vague techniques, such as tagged unions or virtual method tables, the above metaprograms leverage type safety, syntax conciseness, and maintain the exact memory layout of generated code.

Looks interesting? Check out the motivational post for more information.

Getting started

Metalang99 is just a set of header files and nothing else. To use it as a dependency, you need to:

  1. Add metalang99/include to include directories.
  2. Specify -ftrack-macro-expansion=0 (GCC) or -fmacro-backtrace-limit=1 (Clang) to avoid useless macro expansion errors.

If you use CMake, the recommended way is FetchContent:

include(FetchContent)

FetchContent_Declare(
    metalang99
    URL https://github.com/Hirrolot/metalang99/archive/refs/tags/v1.2.3.tar.gz # v1.2.3
)

FetchContent_MakeAvailable(metalang99)

target_link_libraries(MyProject metalang99)

# Disable full macro expansion backtraces for Metalang99.
if(CMAKE_C_COMPILER_ID STREQUAL "Clang")
  target_compile_options(MyProject PRIVATE -fmacro-backtrace-limit=1)
elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU")
  target_compile_options(MyProject PRIVATE -ftrack-macro-expansion=0)
endif()

Optionally, you can precompile headers in your project that rely on Metalang99. This will decrease compilation time because the headers will not be compiled each time they are included.

Tutorial | Examples | User documentation

Happy hacking!

Highlights

  • Macro recursion. Recursive calls behave as expected. In particular, to implement recursion, Boost/Preprocessor just copy-pastes all recursive functions up to a certain limit and forces to either keep track of recursion depth or rely on a built-in deduction; Metalang99 is free from such drawbacks.

  • Almost the same syntax. Metalang99 does not look too alien in comparison with Order PP because the syntax differs insignificantly from usual preprocessor code.

  • Partial application. Instead of tracking auxiliary arguments here and there (as it is done in Boost/Preprocessor), partial application allows to capture an environment by applying constant values first. Besides that, partial application facilitates better reuse of metafunctions.

  • Debugging and error reporting. You can conveniently debug your macros with ML99_abort and report fatal errors with ML99_fatal. The interpreter will immediately finish its work and do the trick.

Philosophy and origins

My work on Poica, a research programming language implemented upon Boost/Preprocessor, has left me unsatisfied with the result. The fundamental limitations of Boost/Preprocessor have made the codebase simply unmaintainable; these include recursive macro calls (blocked by the preprocessor), which have made debugging a complete nightmare, the absence of partial application that has made context passing utterly awkward, and every single mistake that resulted in megabytes of compiler error messages.

Only then I have understood that instead of enriching the preprocessor with various ad-hoc mechanisms, we should really establish a clear paradigm in which to structure metaprograms. With these thoughts in mind, I started to implement Metalang99...

Long story short, it took half of a year of hard work to release v0.1.0 and almost a year to make it stable. As a real-world application of Metalang99, I created Datatype99 exactly of the same form I wanted it to be: the implementation is highly declarative, the syntax is nifty, and the semantics is well-defined.

Finally, I want to say that Metalang99 is only about syntax transformations and not about CPU-bound tasks; the preprocessor is just too slow and limited for such kind of abuse.

Guidelines

  • If possible, assert macro parameters for well-formedness using ML99_assertIsTuple, ML99_assertIsNat, etc. for better diagnostic messages.
  • Prefer the ## token-pasting operator inside Metalang99-compliant macros instead of ML99_cat or its friends, because arguments will nevertheless be fully expanded.
  • Use ML99_todo and its friends to indicate unimplemented functionality.

Publications

Contributing

See CONTRIBUTING.md.

Architecture

See ARCHITECTURE.md.

Idioms

See idioms.md.

Optimisation tips

See optimization_tips.md.

FAQ

Q: What about compile-time errors?

A: Metalang99 is a big step towards understandable compiler diagnostics. It has a built-in syntax checker that tests all incoming terms for validity:

[playground.c]

ML99_EVAL(123)
ML99_EVAL(x, y, z)
ML99_EVAL(v(Billie) v(Jean))

[/bin/sh]

$ gcc playground.c -Imetalang99/include -ftrack-macro-expansion=0
playground.c:3:1: error: static assertion failed: "invalid term `123`"
    3 | ML99_EVAL(123)
      | ^~~~~~~~~
playground.c:4:1: error: static assertion failed: "invalid term `x`"
    4 | ML99_EVAL(x, y, z)
      | ^~~~~~~~~
playground.c:5:1: error: static assertion failed: "invalid term `(0v, Billie) (0v, Jean)`, did you miss a comma?"
    5 | ML99_EVAL(v(Billie) v(Jean))
      | ^~~~~~~~~

Metalang99 can even check for macro preconditions and report an error:

[playground.c]

ML99_EVAL(ML99_listHead(ML99_nil()))
ML99_EVAL(ML99_unwrapLeft(ML99_right(v(123))))
ML99_EVAL(ML99_div(v(18), v(4)))

[/bin/sh]

$ gcc playground.c -Imetalang99/include -ftrack-macro-expansion=0
playground.c:3:1: error: static assertion failed: "ML99_listHead: expected a non-empty list"
    3 | ML99_EVAL(ML99_listHead(ML99_nil()))
      | ^~~~~~~~~
playground.c:4:1: error: static assertion failed: "ML99_unwrapLeft: expected ML99_left but found ML99_right"
    4 | ML99_EVAL(ML99_unwrapLeft(ML99_right(v(123))))
      | ^~~~~~~~~
playground.c:5:1: error: static assertion failed: "ML99_div: 18 is not divisible by 4"
    5 | ML99_EVAL(ML99_div(v(18), v(4)))
      | ^~~~~~~~~

However, if you do something awkward, compile-time errors can become quite obscured:

// ML99_PRIV_REC_NEXT_ML99_PRIV_IF_0 blah(ML99_PRIV_SYNTAX_CHECKER_EMIT_ERROR, ML99_PRIV_TERM_MATCH) ((~, ~, ~) blah, ML99_PRIV_EVAL_)(ML99_PRIV_REC_STOP, (~), 0fspace, (, ), ((0end, ~), ~), ~, ~ blah)(0)()
ML99_EVAL((~, ~, ~) blah)

In either case, you can try to iteratively debug your metaprogram. From my experience, 95% of errors are comprehensible -- Metalang99 is built for humans, not for macro monsters.

Q: What about debugging?

A: See the chapter "Testing, debugging, and error reporting".

Q: What about IDE support?

A: I use VS Code for development. It enables pop-up suggestments of macro-generated constructions but, of course, it does not support macro syntax highlighting.

Q: Compilation times?

A: To run the benchmarks, execute ./scripts/bench.sh from the root directory.

Q: How does it work?

A:

  1. Because macro recursion is prohibited, there is an ad-hoc recursion engine which works by deferring macro expansions and passing continuations here and there.
  2. Upon it, the continuation-passing style interpreter reduces language expressions into final results.
  3. The standard library is nothing but a set of metafunctions implemented using the core metalanguage, i.e. they are to be evaluated by the interpreter.

Q: Why not third-party code generators?

A: See the blog post "What’s the Point of the C Preprocessor, Actually?"

Q: Is it Turing-complete?

A: The C/C++ preprocessor is capable to iterate only up to a certain limit. For Metalang99, this limit is defined in terms of reductions steps: once a fixed amount of reduction steps has exhausted, your metaprogram will not be able to execute anymore.

Q: Why macros if we have templates?

A: Metalang99 is primarily targeted at pure C, and C lacks templates. But anyway, you can find the argumentation for C++ at the website of Boost/Preprocessor.

Q: Where is an amalgamated header?

A: I am against amalgamated headers because of burden with updating. Instead, you can just add Metalang99 as a Git submodule and update it with git submodule update --remote.

Q: Which standards are supported?

A: C99/C++11 and onwards.

Q: Which compilers are tested?

A: Metalang99 is known to work on these compilers:

  • GCC
  • Clang
  • MSVC
  • TCC

metalang99's People

Contributors

hirrolot avatar ritschwumm avatar timgates42 avatar undarkaido avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

metalang99's Issues

[WISH] Lay explanation how this magic works - maybe a blog post?

Hey @Hirrolot,

I was trying to read the implementation of the metalang99 but steam started going out of my ears.
The problem is 100% or receiving end - I lack understanding of C macros and also functional principles you've applied.

I really enjoyed your last blog post "Fueled Evaluation for Decidable Type Checking" for its clarity of thought.
It would be really great to have a similar article that explains metalang99 for smallbrain grug [1].

In particular these questions are interesting:

  • What knowledge of C preporcessor is required even to start understanding Metalang99?
  • How does it really work?
  • What is CPS and why was it a solution? Were there any alternatives to a chosen mechanism?
  • How does CPS solve macro recursion limitation?
  • How to read metalang99 internals? How to teach it to someone else?
  • Can I write some other useful abstraction with it? How to approach such a task?

The knowledge is only good if we can re-transmit and reproduce it. Metalang99 probably goes above the head of the average developer.

Thanks a lot!

[1] https://grugbrain.dev/

Invalid standard detection for `_Static_assert` on MSVC

There is a typo in __STDC_VERSION__:

#elif defined(_MSC_VER) && __STDC__VERSION >= ML99_PRIV_C11_VERSION

Therefore, __STDC__VERSION is substituted with 0, thereby making the condition __STDC__VERSION >= ML99_PRIV_C11_VERSION always evaluate to false. Thus, even if _Static_assert is supported by MSVC, Metalang99 will emit an error:

#error Your compiler doesn't support decent diagnostic messages. \

Feature proposal

Control statements

I was asking myself if implementing custom control statements was a possibility for this project, or if this kind of things completely was up to users to do, on their own.
For example, it could be nice to implement things like "foreach", or along the lines of those proposed here.
I was also thinking of things like :

#define check_in if(((
#define for_only_flags(...) ) | (ML99_LIST_EVAL(ML99_call(ML99_listIntersperse, v(|), ML99_list(v(__VA_ARGS__)))))) == (ML99_LIST_EVAL(ML99_call(ML99_listIntersperse, v(|), ML99_list(v(__VA_ARGS__))))))
#define for_at_least_flags(...) ) & (ML99_LIST_EVAL(ML99_call(ML99_listIntersperse, v(|), ML99_list(v(__VA_ARGS__)))))) == (ML99_LIST_EVAL(ML99_call(ML99_listIntersperse, v(|), ML99_list(v(__VA_ARGS__))))))

while working on my own personal project.
It could be used with user-provided flags like :

#define FLAG1 1U
#define FLAG2 2U
#define FLAG3 4U
// ...
void check_flags_in_var(unsigned int var)
{
    check_in var for_only_flags(FLAG1, FLAG2)
    {
        // do something
    }
    else check_in var for_at_least_flags(FLAG1, FLAG2)
    {
        // ...
    }
// ...
}

There would be (of course) the usual ML99_ namespace and with the good style convention.
If you're okay, that would be a pleasure to contribute to this precious repo.

Personally, I think that could fit the purpose of the lib, since there is already a "Support for C language constructions" with gen.h, although it's not as general purpose as the rest of the library (more particularly if we consider the aforementioned header, wich stays quite general purpose).

Variable introduction macros don't work on C++

If we try to compile the following code on a C++ compiler:

#include <metalang99.h>

int main(void) {
    ML99_INTRODUCE_VAR_TO_STMT(int x = 5)
        (void)x;

    ML99_INTRODUCE_NON_NULL_PTR_TO_STMT(int, x, 0)
        (void)x;
}

We will get:

$ g++ test.c -Iinclude -ftrack-macro-expansion=0
test.c: In function ‘int main()’:
test.c:4:5: error: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]
    4 |     ML99_INTRODUCE_VAR_TO_STMT(int x = 5)
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~
      |     |
      |     void*
test.c:4:5: error: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]
    4 |     ML99_INTRODUCE_VAR_TO_STMT(int x = 5)
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~
      |     |
      |     void*
test.c:7:5: error: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]
    7 |     ML99_INTRODUCE_NON_NULL_PTR_TO_STMT(int, x, 0)
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |     |
      |     void*

Affected macros:

  • ML99_INTRODUCE_VAR_TO_STMT
  • ML99_INTRODUCE_NON_NULL_PTR_TO_STMT

How do you loop over a macro?

That's my problem with this and P99, both are huge convoluted libraries for the preprocessor and every time I try spelunking into these codebases, I just get more confused.

my goal is to use _Pragma(push_macro(MacroName)) to set MacroName to a list of Test Suites while regstering all the suites

then at the end to loop COUNTER times over MacroName with _Pragma(pop_macro(MacroName))

to create a designated initializer of Test Suites that can be written to the executable with linker section magic, and I'm just not sure how to get there.

I've got the linker magic figured out, but I can't figure out how the heck to get the damn looping to work

Can you help?

Don't understand how to recurse

I have read the tutorial and write the code according to example but it doesn't work. I took several hours to try different patterns with v() or without v() but still don't figure out how to use it. So I came here to find some help, here is the code.

#define _GetUnderlyingType(LayerNumber,CurrentType)															\
	std::enable_if_t<_IsEligibleType<CurrentType,LayerNumber,_default_index>::value,									\
	typename _IsEligibleType<CurrentType,LayerNumber,_default_index>::underlying_type>													

#define _ConstructGetUnderlyingType(TotalLayer,StartLayer,T) ML99_natMatchWithArgs(TotalLayer, v(_ConstructGetUnderlyingType_),StartLayer,T)
#define _ConstructGetUnderlyingType_Z_IMPL(StartLayer,T) v(T)
#define _ConstructGetUnderlyingType_S_IMPL(TotalLayer,StartLayer,T)												\
	_GetUnderlyingType(StartLayer-TotalLayer+2,_ConstructGetUnderlyingType(v(TotalLayer),StartLayer,T))												

	ML99_EVAL(_ConstructGetUnderlyingType(v(1), v(1), v(T)));

Change aux file names for compatibility with windows

Hi, I was excited to try out your datatype99. Unfortunately metalang99 (which datatype99 requires) uses file names beginning with aux, such as aux.h and aux.rst. Unfortunately, this severely messes with windows, as windows refuses to recognize files named aux (they are reserved file names, apparently). As such, it is quite difficult to even delete those files.

I can use easily switch to using WSL, but I would greatly appreciate it if the file names could be changed, and this annoyance resolved (or more like worked around 🙂). 🙏

Optimise unsigned integers

Currently, operations on unsigned integers are implemented recursively.

The task is to optimise the unsigned integers module (include/metalang99/uint.h), e.g. make fewer reduction steps. Maybe it would be better to deal with digits (the usual way of calculating) instead of Peano arithmetic.

Potential macro expansion ambiguity in the recursion engine

It seems that the recursion engine relies on one ambiguity in the standard (the issue is about C++ but the C preprocessor is essentially the same):

#define ML99_PRIV_REC_NEXT(next_lvl, choice) ML99_PRIV_REC_NEXT_##choice(next_lvl)

Here, ML99_PRIV_REC_NEXT can expand to ML99_PRIV_REC_<level>, which is then invoked by a caller of ML99_PRIV_REC_NEXT, thus resulting in the ML99_PRIV_REC_NEXT token once again.

However, I haven't noticed any problems with various compilers yet as they seem to treat this ambiguity in the same way. Please, let me know if your compiler doesn't work because of it; to make sure, run tests/eval/rec.c.

issue with ML99_if and clang

I have this version of the metalang99 repository:

c986b02ca97c902fa18cebfd84417f6592c6da68

Here is the code I'm testing:

#include <metalang99.h>

int foo(void) { return ML99_if(v(0), v(123), v(18)); }

here is the command I ran:

clang -I./metalang99/include -E metalangtest.c >out.c

This is what it produced:

int foo(void) { return (0args, ML99_if, (0v, 0), (0v, 123), (0v, 18)); }

This is what I expected:

int foo(void) { return 18; }

clang --version:

Apple clang version 13.0.0 (clang-1300.0.29.30)
Target: x86_64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

Unroll recursion in list.h

As for now, the list operations are implemented naively. The task is to unroll them (see loop unrolling) to improve their performance (at least unroll the mostly used functions).

Rules engine written (at least partially) in metalang99

I've recently kind of fell in love with https://github.com/paranim/pararules . But I'm not a Nim user most of the time and would want something like that in pure C.

I wonder whether some form of the staticRuleset together with the "wrapper macro" to add rules to staticRuleset in compile time would be viable to write in metalang99.

Would it be worth it? What would be the disadvantage(s)?

Thanks for your thoughts, observations, and recommendations!

Issue with ML99_listFromSeq

When trying to use it on one of the sequences mentioned in the top of seq.h doc, I got an error :

ML99_LIST_EVAL(ML99_listFromSeq(v((+, -, *, /)(123)(~))))
error: macro "ML99_cons_IMPL" passed 5 arguments, but takes just 2

Maybe this is just me not understanding something ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.