nim-lang / RFCs

A repository for your Nim proposals.
136 stars 23 forks source link

const should work with ref types #271

Open timotheecour opened 3 years ago

timotheecour commented 3 years ago

fully implemented in PR https://github.com/nim-lang/Nim/pull/15528, see PR for details, tests, examples, and discussion. (RFC was requested here: https://github.com/nim-lang/Nim/pull/15528#issuecomment-706489325)

highlights under this PR:

precedent in other languages

Araq commented 3 years ago

One downside that I can see is this RFC is in direct contrast to https://github.com/nim-lang/RFCs/issues/257 (which was accepted).

Araq commented 3 years ago

If you handle const x = MyRef() like template x(): untyped = MyRef(), what's the gain? People can write the template version too. Programming languages are designed to catch mistakes, if you create a programming language where everything compiles ("this must work, think about generic programming!"), you created a programming language that catches no mistakes.

timotheecour commented 3 years ago

If you handle const x = MyRef() like template x(): untyped = MyRef(), what's the gain? People can write the template version too.

that's not the same at all.

The point is this feature allows caching the result of a potentially expensive computation or something that should be computed just once for correctness.

with https://github.com/nim-lang/Nim/pull/15528 this works:

import std/json

proc fn(file: static string): JsonNode =
  echo "parsing " & file
  const s = staticRead(file)
  result = parseJson(s)

const j = fn("/tmp/myconfig.json") # fn will be called just once

proc main() =
  # CT code can now use `j`, eg:
  const version = j["version"].getStr
  const name = j["name"].getStr

  # RT code can also use `j`, eg:
  echo (version, name)
  echo j.pretty.static
  echo j["name"].getStr.static
  echo j["version"].getStr.static

main()

I don't see how template x(): untyped = MyRef() would help in any way.

I'm using this to great effect in my own patched nim.

Araq commented 3 years ago

And how do you "cache" it? Previously you claimed it doesn't affect GC safety.

timotheecour commented 3 years ago

@Araq

And how do you "cache" it? Previously you claimed it doesn't affect GC safety.

no contradiction; it's cached in the same way it's cached for non-ref types, after const j = expr is evaluated (once), the result is stored in j'ast as a PNode.

Subsequent const accesses (eg const version = j["version"].getStr) use j directly, while subsequent runtime accesses paste the resulting PNode litteral as if it was inlined in the code, eg:

example 1 (from example above)

echo j["name"].getStr.static
# is same as:
const tmp: string = j["name"].getStr
echo tmp # the `tmp` string gets pasted where it's used, as if the code was: echo "foobar"

example 2 (more complex example):

when true:
  type Foo = ref object
    n: int
    lhs, rhs: Foo

  proc fn(n: int): Foo =
    result = Foo(n: n)
    if n > 1:
      if n mod 2 == 0:
        result.lhs = fn(n div 2)
      if n mod 2 == 1:
        result.rhs = fn((n+1) div 2)

  const j1 = fn(20)

  let j2 = j1
    # same as if we inlined the following
    # let j2 = Foo(n: 20, lhs: Foo(n: 10, lhs: Foo(n: 5, rhs: Foo(n: 3, rhs: Foo(n: 2, lhs: Foo(n: 1))))))
    # `fn` is not re-evaluated here 
Araq commented 3 years ago

Thanks. It's growing on me...

Araq commented 3 years ago

It is in direct contrast to #257 (which was accepted) so maybe for #257 we need a .section(readonly) pragma. In fact, .section would be the last missing feature to get us to competitive for embedded programming.

konsumlamm commented 3 years ago

Maybe I'm missing something, but isn't the point of const not to inline the expression, but to precompute it at compile time and then inline the result or store it in the executable? Or is this more like Rust's lazy_static, which computes it at runtime, but only the first time it's accessed? (In the latter case, that seems inconsistent with how const works for other expressions.)

timotheecour commented 3 years ago

const a = foo() doesn't codegen a, only a RT use of a will codegen, eg:

type Foo = ref object
  x1: int # etc; can contain other nested ref types
proc foo(n: int): Foo =
  # potentially expensive computation (potentially non-pure or with CT side effects)  
const a1 = foo(3) # a is not codegen'd but computed at CT; foo(3) is computed just here
const a2 = a1.x1 # foo() is not recomputed here
let a3 = a2 # only a2 is codegen'd here; or, more directly, `let a3 = a1.x1.static`

const a4 = foo(4) # foo(4) computed just here
let a5 = a4 # a5 is codegen'd, using a literal, eg as if `let a5 = Foo(x1: ...)` was written

This simply extends const to work at CT, it otherwise works the same as types that don't contain (nested) ref