RFCs icon indicating copy to clipboard operation
RFCs copied to clipboard

All let {.compileTime.} constructs can now be const instead

Open deech opened this issue 6 years ago • 11 comments

Compile time let and var are currently buggy but due to a recent fix all the following constructs:

let x {.compileTime.} = ...
let (a,b) {.compileTime.} = ...

can now be replaced with:

const x = ...
const (a,b) = ...

They are also available in a static block after declaration:

const x = ...
static:
   echo x

The only thing const disallows vs. let is annotating it with a pragma, so the following does not work:

const x {.myCustomPragma.} = ...

In light of this it may be a good idea to just deprecate let x {.compileTime.} = ....

This doesn't help the var situation but IMO toplevel compile time mutable state should also be disallowed.

deech avatar May 05 '19 10:05 deech

I don't mind removing let x {.compileTime.} but removing toplevel compile-time var would break many of my libraries because I would have no way to do identifier hashing at compile-time:

  • Compiletime hashtables of NimNodes: https://github.com/nim-lang/RFCs/issues/131
  • It is the only way of mangling for C FFI in rare cases (OpenMP) because const generated in templates are not visible for exportc so I need a top-level compile-time var: https://github.com/nim-lang/Nim/issues/9365#issuecomment-429613318

I'm also using those for quick prototyping of an embedded DSL (https://github.com/mratsim/compute-graph-optim/blob/023ddf6d07df0deb1c55722cb30627052bed4b1b/e11_DSL_compiler_seq.nim#L956-L970), due to symbol mangling templates don't work, dirty templates and blocks don't work (https://github.com/nim-lang/Nim/issues/11091#issuecomment-485826614). The ideal solution is proc but that is my second step after quick prototyping.

mratsim avatar May 05 '19 10:05 mratsim

I must be missing something with your C FFI example because:

const foo = "foo"
let notfoo {.exportc: "someprefix" & foo .} = 0

generates:

...
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
	nimfr_("constexportc", "constexportc.nim");
	nimln_(2, "constexportc.nim");
	someprefixfoo = ((NI) 0);
	popFrame();
}

Edit: Got it. The const has to be in a template to repro your issue.

deech avatar May 05 '19 11:05 deech

IMO {.compileTime.} should be merged with static, and the scope issue (https://github.com/nim-lang/Nim/issues/9970#issuecomment-452226661) be resolved like this:

Static without introducing a scope:

static let foo = "ha"
echo foo #At runtime

works because static without a : wouldn't introduce a new scope. Similarily this would work:

static let
  foo = "ha"
  bar = "ho"

echo foo, bar #At runtime

but this would not (already the case):

static:
  let foo = "ha"
  let bar = "ho"

echo foo, bar #Doesnt work at runtime

and this would (also already the case):

static:
  let foo = "ha"
  echo foo #At compiletime

const foo = "ha" could then be an alias for static let = "ha"

This would give a semantic representation of wether a compiletime/static value can "leak" into runtime via scope.

EDIT: All of the above should also apply for var, as compile-time var are very useful/necessary in some situations.

Clyybber avatar May 05 '19 18:05 Clyybber

I am 100% on board. I made exactly this argument on chat about 8 months ago I think and it was ignored/rejected. If the mood is more in favor now I'm happy to turn this into an RFC.

After the examples posted above I agree that today compile time vars are useful but I do think we should work toward finding alternatives for each use case as they come up.

As an example I'm working on having const bindings visible to pragmas in templates to address the C FFI issue.

deech avatar May 05 '19 19:05 deech

I see little value, lots of changes required but nothing that can't be done today will be made possible.

cooldome avatar May 05 '19 20:05 cooldome

I forgot about one key use case that const does not cover compared to let {.compileTime.}: ref object at compileTime for macro input.

Test case:

import macros

type Foo = ref object
  val: int

# const x = Foo(val: 1) # invalid
let x {.compileTime.} = Foo(val: 1) # OK

Edit: Also, for that case I'm sure that {.compileTime.} ref objects will not be present in the binary. I'm not sure what a const ref objects would mean regarding code generation, and also assuming I use a const ref object only for VM operations I don't want it to end up in my binary.

mratsim avatar May 07 '19 08:05 mratsim

After thinking a bit more about it I think it's fine as is, only that {.compileTime.} is a bit clunky syntactically.

  • const serves to bridge compile time and runtime essentially, by embedding its value in the binary
  • {.compileTime.} only exists at compiletime/in the vm
  • declarations inside a static: block are essentially the same as {.compileTime.} but with a scope

So const a = someexpr is more like the equivalent of let a = static: someexpr, except that a is also accessible at compile time.

So only the syntax is a bit inconsistent/confusing. One could rename {.compileTime.} to {.static.} or introduce the static (without a :) prefix for let and var sections.

Clyybber avatar May 07 '19 09:05 Clyybber

{.static.} sounds good to me. Plus a paragraph about the distinction between what is run in the VM and never present in the binary (static) and what can end up in the binary if it is used (otherwise it is optimised away.

For starter we can proceed on this PR: https://github.com/nim-lang/Nim/issues/9817 (this will break some of my libraries in CI but should be an easy fix).

I'm still unsure about the incremental compilation interaction as mentionned by @krux02 here and in this {.compileTime.} bugfix https://github.com/nim-lang/Nim/issues/9817#issuecomment-447869197.

Tagging also the RFC on compileTime functions https://github.com/nim-lang/Nim/issues/8051

mratsim avatar May 07 '19 11:05 mratsim

@mratsim Didn't know about that use of let. Interesting. @Clyybber Using a pragma to indicate which construct should be evaluated at compile vs. run time is also problematic because mixing them in the same syntactic construct is very confusing and makes things more complex than necessary, eg:

let (a,b {.compileTIme.}, c) = (1,2,b+1)

It's not clear that c relies on a compile time value, vs. eg.

static let b = 2
let (a,c) = (1, b+1)

One compromise could be to mandate that {.compileTime.} cannot be interleaved with runtime bindings.

Another problem with leaving it as a pragma is that {.compileTime.} values and runtime value share the same namespace when they are clearly separate phases, eg. we can't do:

let x {.compileTIme.} = ...
let x = ...

In any case my goal isn't to break people's existing code and invalidate documentation just for the sake of it, I just think the static introspection and compile time features of Nim are enough of a differentiator that it's worth getting the UX and VM bulletproof as possible before Nim gets wide adoption.

deech avatar May 07 '19 14:05 deech

@deech I agree; I'd prefer the syntax without the pragma; but there doesn't seem to be consensus; and it would probably require an RFC first. Anyways, I have a proof-of-concept implementation here: https://github.com/Clyybber/Nim/commit/af2676de160566482b8c25ba88457c982b11fa36 So far it works for static let static proc and static func. (not sure why not for static var)

Clyybber avatar May 07 '19 16:05 Clyybber