Commit Graph

2 Commits

Author SHA1 Message Date
Christopher Haster c3533ab816 Some progress, with deferred attributes taking shape
Ran into an interesting macro-related bug. Turns out the way we are
doing implicit prefixing in TAG/ATTR macros sort of breaks how C macros
work a bit. The following does not compile:

  lfsr_mdir_commit(lfs, &file->m.mdir, LFSR_ATTRS(
          LFSR_ATTR(file->m.mdir.mid, DEFER, 0, DEFER(
              (lfsr_rbyd_t*)&file->inlined,
              LFSR_ATTR(file->buffer_pos,
                  DEFERRED(INLINED), +file->buffer_size, BUF(
                      file->buffer, file->buffer_size))))));

Or to distill it down, this does not compile:

  #define LFSR_ATTR(_data)  (LFSR_##_data)
  #define LFSR_DEFER(_data) (LFSR_##_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(DEFER(ATTR(DATA(1))));

But this does:

  #define LFSR_ATTR(_data)  (_data)
  #define LFSR_DEFER(_data) (_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(LFSR_DEFER(LFSR_ATTR(LFSR_DATA(1))));

Why? Well it turns out the whole way nested C macro's work is a big
hack.

A very reasonable design decision in C is to disallow recursive macro
expansions. Unlike C++, we don't want our preprocessor to suddenly stack
overflow. This rule is enforced by stopping macro expansion when a macro
contains itself. For example:

  #define A() B()
  #define B() A()

  A()

Expands to:

  A()
      -> B()
      -> A() (stops, probably erroring with 'A' undeclared)

But it _is_ common to want to recursively expand macro arguments. Macros
are a part of C's syntax after all, and users usually expect
expressions, such as arguments, to be context-free:

  #define A(x) (x) + 1

  A(A(A(A(A(0)))))

Naively this would expand to:

  A(A(A(A(A(0)))))
      -> (A(A(A(A(0))))) + 1 (stops)

The big hack that makes this work in C's preprocessor is the "Argument
prescan". Instead of expanding the "called" macro first, we expand any macro
inside our argument list, _then_ expand the "called" macro, and _then_
expand any new macros produced as a result of the expansion again just
for good measure.

So the above actually expands to:

  A(A(A(A(A(0)))))
      -> A(A(A(A((0) + 1))))
      -> A(A(A(((0) + 1) + 1)))
      -> A(A((((0) + 1) + 1) + 1))
      -> A(((((0) + 1) + 1) + 1) + 1)
      -> (((((0) + 1) + 1) + 1) + 1) + 1

This is still recursive actually! But the recursion is limited to the
actual length of the source code, so the developers likely thought this
was a reasonable tradeoff.

But what does this mean for our implicit prefixing?

  #define P_A(x) P_##x
  #define P_B(x) P_##x
  #define P_C(x) (x)

  P_A(B(A(C(0))))

None of A, B, C are in scope without prefixes, so they get expanded
after the "called" macro's expansion:

  P_A(B(A(C)))
      -> P_B(A(C(0)))
      -> P_A(C(0)) (stops)

But this breaks when we hit the nested P_A macro.

---

For now I've gone with the temporary, and extra hacky, solution of
introducing a second LFSR_ATTR_ macro. This nesting of ATTR macros only
happens because of shrubs, and only ever goes 2 layers deep.

In the future maybe we should move away from implicit prefixing. They
have a few rough corners and may be a bit confusing for anyone new to
the code.
2023-10-13 22:00:55 -05:00
Christopher Haster c74ec1c133 Initial commit of basic file creation
Currently limited to inlined files and only simpler truncate-writes.

But still this lets us test file creation/deletion.

This is also enough logic to make it clear that, even though we have
some powerful high-level primitives, mapping file operations onto these
is still going to be non-trivial.
2023-09-17 11:04:44 -05:00