Lua Notes

nelua q&a
Login

Questions

  1. How is 'Nelua' pronounced?
  2. What limitations come with disabling the GC?
  3. What C standard does Nelua adhere to? Can I affect the standards-compliance of the codegen?
  4. Does Nelua support tail-call optimization? Should I write like it's a Scheme?
  5. How can a method accept self as T instead of *T?
  6. Does Nelua have named arguments?
  7. How can you slice an array, like arr[start : end]

How is 'Nelua' pronounced?

Like this.

Official answer: like translate.google.com says.

GC limitations

There aren't any. Nelua isn't like D where you have a distinct sublanguage in -betterC, or lots of restrictions with @nogc. Only five libraries check pragmas.nogc, and three of them are allocator libraries:

  1. allocators.default chooses between the GC and malloc/free:
    ## if not pragmas.nogc then -- GC enabled
      require 'allocators.gc'
      ## context.rootscope.symbols.default_allocator = gc_allocator
      ## context.rootscope.symbols.DefaultAllocator = GCAllocator
    ## else -- GC disabled
      require 'allocators.general'
      ## context.rootscope.symbols.default_allocator = general_allocator
      ## context.rootscope.symbols.DefaultAllocator = GeneralAllocator
    ## end
  2. C.threads rejects the GC
  3. coroutine has some GC interaction that's only done if there's a GC to interact with

There shouldn't any leaks when using the stdlib. Although without a GC you're responsible for cleaning up resources returned from stdlib functions.

NB. this doesn't mean that Nelua trivializes memory management. I've used -S/--sanitize and valgrind more with Nelua than ever before. It's a different experience and not absolutely a worse one.

What C standard does Nelua adhere to?

"Nelua code generator does not target C standards, it targets GCC and Clang" - of a C23 niltype definition.

Which links to this detailed answer:

  1. "Nelua does not depend on C11. It does emit some C11 specific code like _Noreturn, _Static_assert, but they are all used through ifdefs so the code can also compile on old compilers that don't support C11."
  2. you can avoid a C library it it has a dependency you don't like
  3. (there is also the 'threadlocal' annotation that expressly depends on C11)
  4. "But I can't say Nelua depends only on C99, and neither C11, because the C code generator depends on some C extensions are available in most C compilers,"
  5. "I would say Nelua targets Clang/GCC/TCC,"

TCO support?

Nelua doesn't treat recursion specially, and emits C that's very similar to what was written. So in short: no, you shouldn't write it like it's a Scheme.

But consider:

local function recur(n: number): void <cexport>
  print(n)
  recur(n+1)
end
recur(0)

This has an explicit return type because Nelua requires that for recursive functions. It has <export> only to make it easier to find the code in the resulting binary.

How does this run normally?

$ nelua recur.nelua
...
261684.0
261685.0
261686.0
Segmentation fault

It runs out of stack and crashes.

With -S/--sanitize this is shown in detail:

$ nelua -S recur.nelua
AddressSanitizer:DEADLYSIGNAL
=================================================================
==8477==ERROR: AddressSanitizer: stack-overflow on address 0x7fff89b43ff8 (pc 0x7f9aebe6b9c8 bp 0x7fff89b442d0 sp 0x7fff89b43fd0 T0)
    #0 0x7f9aebe6b9c8 in __mpn_divrem stdlib/divrem.c:47
    #1 0x7f9aebe71754 in hack_digit stdio-common/printf_fp.c:187
    #2 0x7f9aebe72823 in __GI___printf_fp_l stdio-common/printf_fp.c:942
    #3 0x7f9aebe7cf5c in __printf_fp_spec stdio-common/vfprintf-internal.c:354
    #4 0x7f9aebe7cf5c in __vfprintf_internal stdio-common/vfprintf-internal.c:1061
    #5 0x7f9aebe9d667 in __vsnprintf_internal libio/vsnprintf.c:114
    #6 0x7f9aec87433d in __interceptor_vsnprintf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1665
    #7 0x7f9aec8745be in __interceptor_snprintf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1736
    #8 0x55b4017e8325 in nelua_print_1 .../.cache/nelua/recur.c:98
    #9 0x55b4017e8a74 in recur_recur .../.cache/nelua/recur.c:116
    #10 0x55b4017e8a94 in recur_recur .../.cache/nelua/recur.c:117
...
    #247 0x55b4017e8a94 in recur_recur .../.cache/nelua/recur.c:117
    #248 0x55b4017e8a94 in recur_recur .../.cache/nelua/recur.c:117

SUMMARY: AddressSanitizer: stack-overflow stdlib/divrem.c:47 in __mpn_divrem
==8477==ABORTING

How does a release build perform?

$ nelua -r recur.nelua
...

It never crashes, because the C compiler optimized the tail-recursive function into an iterative one. Note: there is no difference in the emitted C code. The only difference is in the C compiler flags: -O2 -DNDEBUG are passed instead of the default which is just -g.

How does that function disassemble?

$ objdump -dwr ~/.cache/nelua/recur
...
0000000000001180 <recur_recur>:
    1180:	41 56                	push   %r14
    1182:	66 49 0f 7e c6       	movq   %xmm0,%r14
    1187:	55                   	push   %rbp
    1188:	48 8d 2d 75 0e 00 00 	lea    0xe75(%rip),%rbp        # 2004 <_IO_stdin_used+0x4>
    118f:	53                   	push   %rbx
    1190:	48 83 ec 30          	sub    $0x30,%rsp
    1194:	48 89 e3             	mov    %rsp,%rbx
    1197:	66 0f 1f 84 00 00 00 00 00 	nopw   0x0(%rax,%rax,1)
    11a0:	48 89 ea             	mov    %rbp,%rdx
    11a3:	48 89 df             	mov    %rbx,%rdi
    11a6:	66 49 0f 6e c6       	movq   %r14,%xmm0
    11ab:	be 2f 00 00 00       	mov    $0x2f,%esi
    11b0:	b8 01 00 00 00       	mov    $0x1,%eax
    11b5:	c6 44 24 2f 00       	movb   $0x0,0x2f(%rsp)
    11ba:	e8 71 fe ff ff       	call   1030 <snprintf@plt>
    11bf:	48 89 da             	mov    %rbx,%rdx
    11c2:	4c 63 c0             	movslq %eax,%r8
    11c5:	4a 8d 3c 03          	lea    (%rbx,%r8,1),%rdi
    11c9:	eb 1c                	jmp    11e7 <recur_recur+0x67>
    11cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
    11d0:	8d 71 d0             	lea    -0x30(%rcx),%esi
    11d3:	40 80 fe 09          	cmp    $0x9,%sil
    11d7:	76 05                	jbe    11de <recur_recur+0x5e>
    11d9:	80 f9 2d             	cmp    $0x2d,%cl
    11dc:	75 24                	jne    1202 <recur_recur+0x82>
    11de:	48 83 c2 01          	add    $0x1,%rdx
    11e2:	48 39 fa             	cmp    %rdi,%rdx
    11e5:	74 07                	je     11ee <recur_recur+0x6e>
    11e7:	0f b6 0a             	movzbl (%rdx),%ecx
    11ea:	84 c9                	test   %cl,%cl
    11ec:	75 e2                	jne    11d0 <recur_recur+0x50>
    11ee:	8d 50 02             	lea    0x2(%rax),%edx
    11f1:	83 c0 01             	add    $0x1,%eax
    11f4:	42 c6 04 04 2e       	movb   $0x2e,(%rsp,%r8,1)
    11f9:	48 98                	cltq
    11fb:	4c 63 c2             	movslq %edx,%r8
    11fe:	c6 04 04 30          	movb   $0x30,(%rsp,%rax,1)
    1202:	48 8b 0d 27 2e 00 00 	mov    0x2e27(%rip),%rcx        # 4030 <stdout@GLIBC_2.2.5>
    1209:	4c 89 c2             	mov    %r8,%rdx
    120c:	be 01 00 00 00       	mov    $0x1,%esi
    1211:	48 89 df             	mov    %rbx,%rdi
    1214:	e8 47 fe ff ff       	call   1060 <fwrite@plt>
    1219:	48 8b 35 10 2e 00 00 	mov    0x2e10(%rip),%rsi        # 4030 <stdout@GLIBC_2.2.5>
    1220:	bf 0a 00 00 00       	mov    $0xa,%edi
    1225:	e8 16 fe ff ff       	call   1040 <fputc@plt>
    122a:	48 8b 3d ff 2d 00 00 	mov    0x2dff(%rip),%rdi        # 4030 <stdout@GLIBC_2.2.5>
    1231:	e8 1a fe ff ff       	call   1050 <fflush@plt>
    1236:	f2 0f 10 0d d2 0d 00 00 	movsd  0xdd2(%rip),%xmm1        # 2010 <_IO_stdin_used+0x10>
    123e:	66 49 0f 6e d6       	movq   %r14,%xmm2
    1243:	f2 0f 58 ca          	addsd  %xmm2,%xmm1
    1247:	66 49 0f 7e ce       	movq   %xmm1,%r14
    124c:	e9 4f ff ff ff       	jmp    11a0 <recur_recur+0x20>

You can see quite a lot here, but there's some I/O and then on the very last line of the function a jmp to 11a0, near the top of the function.

How can a method accept self as T instead of *T?

The two foo:bar syntaxes are strictly unrelated.

-- these are equivalent:
function T.foo(self: *T) ... end
function T:foo() ... end

-- these are equivalent:
obj.foo(&obj)
obj:foo()

So you can use the second calling syntax even if you didn't use it when defining the function:

local Note = @enum{C, D, E, F, G, A, B}
function Note.show(self: Note) 
  ## for _, field in ipairs(Note.value.fields) do                         
    if wf == #|field.name|# then return #[field.name]# end           
  ## end
  assert(false, 'invalid Note')      
  return ''
}  
check(Note.show(Note.C) == 'C') -- this works
check(Note.C:show() == 'C')     -- and so does this

Does Nelua have named arguments?

It doesn't, but you can have functions that accept records instead of arguments, which is still pretty efficient, still type-checked, and has minimal extra syntax:

require 'string'
local function greet(args: record{name: string, traditional: boolean})
  local name = args.name == '' and 'world' or args.name
  local s: string
  defer s:destroy() end
  if args.traditional then
    s = 'hello ' .. name
  else
    s = string.format('Hello, %s!', name)
  end
  print(s)
end

greet{'Bob'}               -- Hello, Bob!
greet { traditional=true } -- hello world
greet{'world', true}       -- hello world

Note the lack of a @ before the record{} definition.

How can you slice an array, like arr[start : end]

-- option 1. construct a span
require 'span'
local a1: []integer = {1, 2, 3, 4, 5}
local a2: span(integer) = {data=&a1[1], size=3}
a2[0] = 0
a2[2] = 0
for i, v in ipairs(a1) do
  print(i, v)
end

-- option 2. span:sub()
require 'span'
local a1: []integer = {1, 2, 3, 4, 5}
local a2: span(integer) = a1
a2 = a2:sub(1, 4)
a2[0] = 0
a2[2] = 0
for i, v in ipairs(a1) do
  print(i, v)
end

-- option 3. terse span:sub()
require 'span'
local a1: []integer = {1, 2, 3, 4, 5}
local a2 = (@span(integer))(a1):sub(1, 4)
a2[0] = 0
a2[2] = 0
for i, v in ipairs(a1) do
  print(i, v)
end