edubart / nelua-lang

Minimal, efficient, statically-typed and meta-programmable systems programming language heavily inspired by Lua, which compiles to C and native code.
https://nelua.io
MIT License
1.99k stars 64 forks source link

Reassigned 'auto' parameter, inferred to have type 'nilptr', compiles to assignment to NULL #231

Closed jrfondren closed 10 months ago

jrfondren commented 10 months ago

Code example

local function chars(s: string, state: auto)
  local stop = false
  for i=1, #s do
    stop, state = false, nilptr
  end
end

chars('abc', nilptr)

Current behavior

$ nelua nullasgn2.nelua
/home/jfondren/.cache/nelua/nullasgn2.c: In function ‘nullasgn2_chars_1’:
/home/jfondren/.cache/nelua/nullasgn2.c:98:10: error: lvalue required as left operand of assignment
   98 |     NULL = _asgntmp_2;
      |          ^
error: C compilation for '/home/jfondren/.cache/nelua/nullasgn2' failed

Another example

This results in a compilation to NULL = NULL;

local function chars(s: string, state: auto)
  for i=1, #s do
    state = nilptr
  end
end

chars('abc', nilptr)

Workaround

Almost any change will result in working code or a clean error from Nelua instead of this error from C. Giving state an explicit stop, or assigning something other than nilptr. In this example, nil also works. The error was naturally arrived at by

  1. demonstrating iteration with higher order functions, which requires explicit handling of state
  2. using an example where the explicitly-handled state wasn't actually needed
  3. considering nilptr to be a reasonable "don't care" value.

Environment

x86_64 linux Nelua 0.2.0-dev Build number: 1588 Git date: 2023-09-16 16:20:44 -0300 Git hash: 596fcca5c77932da8a07c249de59a9dff3099495 Semantic version: 0.2.0-dev.1588+596fcca5 Copyright (C) 2019-2022 Eduardo Bart (https://nelua.io/)

stefanos82 commented 10 months ago

In Overview documentation, in Pointer section we can read

local n = nilptr -- a generic pointer, initialized to nilptr
local p: pointer -- a generic pointer to anything, initialized to nilptr
local i: *integer -- pointer to an integer

which in other words, what your code is actually doing is indeed trying to assign NULL inside NULL; but if you replace your auto to pointer, it compiles as expected.

This is the generated C code with the usage of auto:

static void tmp_chars_1(nlstring s, void* state);
static int nelua_main(int argc, char** argv);
/* ------------------------------ DEFINITIONS ------------------------------- */
void tmp_chars_1(nlstring s, void* state) {
  for(intptr_t i = 1, _end = ((intptr_t)(s).size); i <= _end; i += 1) {
    NULL = NULL;
  }
}

and this is with the usage of pointer instead:

static void tmp_chars(nlstring s, void* state);
static int nelua_main(int argc, char** argv);
/* ------------------------------ DEFINITIONS ------------------------------- */
void tmp_chars(nlstring s, void* state) {
  for(intptr_t i = 1, _end = ((intptr_t)(s).size); i <= _end; i += 1) {
    state = (void*)NULL;
  }
}

When I read your code I said to myself: "wait, isn't state going to become nilptr and you want to assign nilptr in it? Is this even allowed?" and decided to run the aforementioned tests.

jrfondren commented 10 months ago

I don't see how that part of the overview implies what you say it does. I know that nilptr is NULL. What I wanted to do was pass NULL to the function. The state parameter, though, is not NULL: it is a variable with the value of NULL, and assignment to it should only replace that value and not write to NULL. This is akin to replacing to contents of a CPU register, not writing to memory. Similar to this C program:

include <stdlib.h>
void f(void *p) { p = NULL; }
int main() {
        f(NULL);
        return 0;
}

Or, maybe with auto it isn't a variable with the value of NULL. I don't get that part. But it seems like an error that Nelua and not the C compiler shouldn't be reporting.

stefanos82 commented 10 months ago

Hmm...yes, you have a point :thinking:

Update: The following code helps me understand the logic a bit more, but I could be wrong here.

Only @edubart can enlighten us with this behavior.

do
  local function typenameof(x: auto): string
    return #[tostring(x.type)]#
  end

  local function tellmemytype(x: auto)
    print('my type is:', typenameof(x))
    x = nilptr
    print('my type now is:', typenameof(x))
  end

--  local foo = nilptr
--  tellmemytype(foo)
  tellmemytype(nilptr)
end

The way I interpret tellmemytype()'s behavior is that I pass it a type nilptr whereas I should have had passed it a variable, because this is what the auto does behind the scenes; it deduces to type based on user input as value.

If I comment it out and uncomment the two lines above it, it returns pointer as printed type.

I hope I've got it right :confused:

stefanos82 commented 10 months ago

So, after some further investigation, it seems like that indeed nilptr is an rvalue, it's not a type; its type should be pointer.

What helped me realized this was the flag --print-analyzed-ast.

Call {
        attr = {
          calleesym = "tellmemytype: function(x: nilptr): void",
          calleetype = "function(x: nilptr): void",
          polyeval = "table: 0x7f5114df7a00",
          pseudoargattrs = "table: 0x7f5114dfae40",
          pseudoargtypes = "table: 0x7f5114dfae00",
          type = "void",
        },
        {
          Nilptr {
            attr = {
              comptime = true,
              type = "nilptr",
            },
          }
        },
       ...
edubart commented 10 months ago

Fixed in https://github.com/edubart/nelua-lang/commit/a60b9fe2a5092fbb4ee7fd4541440fccccbcb6e2

stefanos82 commented 10 months ago

@edubart I have just tested this commit and the generated C code for definition looks like this:

void tmp_chars_1(nlstring s, void* state) {
  bool stop = false;
  for(intptr_t i = 1, _end = ((intptr_t)(s).size); i <= _end; i += 1) {
    bool _asgntmp_1 = false;
    stop = _asgntmp_1;
  }
}

Excuse my ignorance, but shouldn't the aforementioned code look something like

void tmp_chars_1(nlstring s, void* state) {
  bool stop = false;
  for(intptr_t i = 1, _end = ((intptr_t)(s).size); i <= _end; i += 1) {
    bool _asgntmp_1 = false;
    stop = _asgntmp_1;
    state = (void *) NULL;
  }
}
jrfondren commented 10 months ago

Pass some other pointers. The function that doesn't assign state to NULL is the one that's specialized to state already being NULL.

stefanos82 commented 10 months ago

Pass some other pointers. The function that doesn't assign state to NULL is the one that's specialized to state already being NULL.

In other words, in

local function chars(s: string, state: auto)
  local stop = false
  for i=1, #s do
    stop, state = false, nilptr
  end
end

chars('abc', nilptr)

because we pass a nilptr in chars(), the compiler gets informed to ignore the ,nilptr part from the for loop?

edubart commented 10 months ago

because we pass a nilptr in chars(), the compiler gets informed to ignore the ,nilptr part from the for loop?

Correct, the thing is already nilptr, so the compiler eliminates the code.

stefanos82 commented 10 months ago

...wait, you have applied Dead Code Elimination in AST mechanism, before emitting C code?! Bruh! O.o