Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #8519 T.distinctBase to reverse T = distinct A #8531

Merged
merged 9 commits into from
Aug 10, 2018

Conversation

timotheecour
Copy link
Member

@mratsim
Copy link
Collaborator

mratsim commented Aug 4, 2018

Usage

I'd like something to undistinct variables as well, current usage would require:

var x: distinct seq[seq[int]] # replace by a complex type
(x.type.undistinct)(x)

for conversion

Simple Tests

There should be a test case with generics and static type.

I checked that generics work:

import typetraits
type
  Foo[T] = distinct seq[T]
var a: Foo[int]
doAssert undistinct(type a) is seq[int]

Stress test

Unfortunately as-is undistinct doesn't pass my "type system stress test" which combines macro, static and generics.

Super complex recursive big int type taken from Stint.

import macros, typetraits

macro uintImpl*(bits: static[int]): untyped =
  # Release version, StUint[64] = uint64.
  assert (bits and (bits-1)) == 0, $bits & " is not a power of 2"
  assert bits >= 8, "The number of bits in a should be greater or equal to 8"

  if bits >= 128:
    let inner = getAST(uintImpl(bits div 2))
    result = newTree(nnkBracketExpr, ident("UintImpl"), inner)
  elif bits == 64:
    result = ident("uint64")
  elif bits == 32:
    result = ident("uint32")
  elif bits == 16:
    result = ident("uint16")
  elif bits == 8:
    result = ident("uint8")
  else:
    error "Fatal: unreachable"

macro undistinct*(T: typedesc, recursive: static[bool] = false): untyped =
  ## reverses ``type T = distinct A``

  let typeNode = getTypeImpl(T)
  expectKind(typeNode, nnkBracketExpr)
  if $typeNode[0] != "typeDesc":
    error "expected typeDesc, got " & $typeNode[0]
  var typeSym = typeNode[1]
  if not recursive:
    let impl = getTypeImpl(typeSym)
    if $impl.typeKind != "ntyDistinct":
      error "type is not distinct"
    getTypeInst(impl[0])
  else:
    while true:
      let impl = getTypeImpl(typeSym)
      if $impl.typeKind != "ntyDistinct":
        typeSym = impl
        break
      typeSym=getTypeInst(impl[0])
    typeSym

type
  # ### Private ### #
  BaseUint* = UintImpl or SomeUnsignedInt

  UintImpl*[Baseuint] = object
    when system.cpuEndian == littleEndian:
      lo*, hi*: BaseUint
    else:
      hi*, lo*: BaseUint

  Uint[bits: static[int]] = distinct uintImpl(bits)



var a: Uint[128]


echo undistinct(type a)
# Error: cannot instantiate UintImpl
# got: <uint64>
# but expected: <Baseuint>

This is another case of https://github.com/nim-lang/Nim/issues/7719 / nim-lang/RFCs#44

It is very easy to get Cannot instantiate or type mismatch because of forgetting to convert the NimSym given by getTypeInst to NimIdent if they are not used raw.

undistinct returns NimSym due to getTypeInst.

Instead making sure to generate fresh idents work.

import macros, typetraits

macro uintImpl*(bits: static[int]): untyped =
  # Release version, StUint[64] = uint64.
  assert (bits and (bits-1)) == 0, $bits & " is not a power of 2"
  assert bits >= 8, "The number of bits in a should be greater or equal to 8"

  if bits >= 128:
    let inner = getAST(uintImpl(bits div 2))
    result = newTree(nnkBracketExpr, ident("UintImpl"), inner)
  elif bits == 64:
    result = ident("uint64")
  elif bits == 32:
    result = ident("uint32")
  elif bits == 16:
    result = ident("uint16")
  elif bits == 8:
    result = ident("uint8")
  else:
    error "Fatal: unreachable"

proc replaceNodes*(ast: NimNode): NimNode =
  # Replace NimIdent and NimSym by a fresh ident node
  proc inspect(node: NimNode): NimNode =
    case node.kind:
    of {nnkIdent, nnkSym}:
      return ident($node)
    of nnkEmpty:
      return node
    of nnkLiterals:
      return node
    else:
      var rTree = node.kind.newTree()
      for child in node:
        rTree.add inspect(child)
      return rTree
  result = inspect(ast)

macro undistinct*(T: typedesc, recursive: static[bool] = false): untyped =
  ## reverses ``type T = distinct A``
  runnableExamples:
    import typetraits
    type T = distinct int
    doAssert undistinct(T) is int
    doAssert: not compiles(undistinct(int))
    type T2 = distinct T
    doAssert undistinct(T2, recursive = false) is T
    doAssert undistinct(int, recursive = true) is int
    doAssert undistinct(T2, recursive = true) is int

  let typeNode = getTypeImpl(T)
  expectKind(typeNode, nnkBracketExpr)
  if $typeNode[0] != "typeDesc":
    error "expected typeDesc, got " & $typeNode[0]
  var typeSym = typeNode[1]
  if not recursive:
    let impl = getTypeImpl(typeSym)
    if $impl.typeKind != "ntyDistinct":
      error "type is not distinct"
    getTypeInst(impl[0]).replaceNodes # <---- fresh ident generator
  else:
    while true:
      let impl = getTypeImpl(typeSym)
      if $impl.typeKind != "ntyDistinct":
        typeSym = impl
        break
      typeSym=getTypeInst(impl[0])
    typeSym.replaceNodes # <---- fresh ident generator

type
  # ### Private ### #
  BaseUint* = UintImpl or SomeUnsignedInt

  UintImpl*[Baseuint] = object
    when system.cpuEndian == littleEndian:
      lo*, hi*: BaseUint
    else:
      hi*, lo*: BaseUint

  Uint[bits: static[int]] = distinct uintImpl(bits)

var a: Uint[128]

echo undistinct(type a) # UintImpl[system.uint64]

@Araq
Copy link
Member

Araq commented Aug 4, 2018

undistinct should be called baseType, that is the term the manual uses. Alternatively it can be called skipDistinct.

@timotheecour
Copy link
Member Author

timotheecour commented Aug 4, 2018

baseType is too confusing, already has different meanings (eg would make it hard to search). skipDistinct sounds weird but at least has distinct in it. I changed to distinctBase which is better IMO, combining the requirements of having distinct, and having base of baseType

@timotheecour timotheecour changed the title fix #8519 T.undistinct to revese T = distinct A fix #8519 T.distinctBase to revese T = distinct A Aug 4, 2018
@timotheecour timotheecour changed the title fix #8519 T.distinctBase to revese T = distinct A fix #8519 T.distinctBase to reverse T = distinct A Aug 4, 2018
@timotheecour
Copy link
Member Author

@mratsim
[note in meantime, i changed undistinct to another name]

I'd like something to undistinct variables as well, current usage would require:
(x.type.undistinct)(x)

I agree, let's leave it for another PR though:

var x: distinct seq[seq[int]] # replace by a complex type
let y=x.undistinct
# would translate to:
let y=cast[x.type.undistinct](x)
# seems more efficient than `let y=(x.type.undistinct)(x) ?`

@timotheecour
Copy link
Member Author

timotheecour commented Aug 4, 2018

@mratsim ok it was easy and useful enough so I added the distinctBase overload for variables:

type T = distinct int
var a: T = T(1)
let b = a.distinctBase

NOTE: this PR reinforces my opinion that generics should only be used for IFTI (see https://github.com/nim-lang/Nim/issues/7517)
it would've been ambiguous if I had use distinctBase[T](recursive: static[bool] = false) instead of macro distinctBase*(T: typedesc, recursive: static[bool] = false) because of the generic overload that operates on variables

@timotheecour
Copy link
Member Author

@mratsim thanks a bunch for your stress test example and implementation fix! I've added your fix, cleaned it up a bit, and added a slightly simplified version of your stress test.

PTAL

@GULPF
Copy link
Member

GULPF commented Aug 5, 2018

let y=cast[x.type.undistinct](x)
# seems more efficient than `let y=(x.type.undistinct)(x) ?`

Why would it be more efficient? Conversion from a distinct type shouldn't generate any code at all. cast is unsafe and should be avoided if possible.

@timotheecour
Copy link
Member Author

timotheecour commented Aug 5, 2018

EDIT wasnt sure, will fix DONE

proc inspect(node: NimNode): NimNode =
case node.kind:
of nnkIdent, nnkSym:
return ident($node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use result instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

of nnkIdent, nnkSym:
return ident($node)
of nnkEmpty, nnkLiterals:
return node
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use result instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

var rTree = node.kind.newTree()
for child in node:
rTree.add inspect(child)
return rTree
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use result instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return rTree
result = inspect(ast)

macro distinctBase*(T: typedesc, recursive: static[bool] = false): untyped =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should always be recursive. If I want to skip the 'distinct' indirections, I want to skip all of them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @mratsim @Araq

I think it should always be recursive. If I want to skip the 'distinct' indirections, I want to skip all of them.

same rationale as having both getTypeImpl and getImpl, plus other constructs where we want a direct vs a recursive behavior.

also, recursive = false is a saner default IMO:
distinctBase(T) will give clean compile error in case T is not a distinct type (ie, catches bugs)
distinctBase(T, recursive = true) will work (and recurse if applies), analog to the difference between "mkdir" and "mkdir -p"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well indeed, I don't see the point of "mkdir" vs "mkdir -p", just create the directory recursively and stop wasting my time. ;-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it to be always recursive for the sake of moving on with this PR (but comments like "stop wasting my time" are not helpful as they don't address the points I mentioned: eg, some applications require the non-recursive getImpl and cannot work with its recursive variant getTypeImpl)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, but "stop wasting my time" referred to mkdir's design.

else:
while true:
let impl = getTypeImpl(typeSym)
if $impl.typeKind != "ntyDistinct":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is if impl.typeKind != ntyDistinct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

var typeSym = typeNode[1]
if not recursive:
let impl = getTypeImpl(typeSym)
if $impl.typeKind != "ntyDistinct":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is if impl.typeKind != ntyDistinct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

typeSym=getTypeInst(impl[0])
typeSym.replaceNodes

proc distinctBase*[T](a: T, recursive: static[bool] = false): auto =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this proc? Please remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hint why is this suspicious: You cannot even write down its return type explicitly.

Copy link
Collaborator

@mratsim mratsim Aug 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To convert a variable without needing to do:

(type a.type.disctinctBase)(a), you would just use a.distinctBase instead.

The signature can be changed to:

template distinctBase*[T](a: T, recursive: static[bool] = false): distinctBase(T, recursive) =
  ## converts a distinct variable to it's original type
  runnableExamples:
    type T = distinct int
    var a: T = T(1)
    let b = a.distinctBase
    doAssert b is int
    doAssert b == 1
  distinctBase(T, recursive)(a)

Here I think func {.inline.} or template are better than simple proc, it's basically a no-op in the C backend.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Araq indeed, this feature was suggested by @mratsim and I agree with him that it's useful.

Changed to Here I think func {.inline.} (not template, according to rule of preferring procs/funcs whenever possible)

basically a no-op in the C backend

I would hope with high enough optimization level the {.inline.} would not make a difference though

You cannot even write down its return type explicitly.

that's because of #8545

so I kept auto

@timotheecour
Copy link
Member Author

PTAL, all comments addressed

@timotheecour
Copy link
Member Author

PTAL, all 2nd round comments addressed

typeSym.replaceNodes

# using `auto` return pending https://github.com/nim-lang/Nim/issues/8551
func distinctBase*[T](a: T): auto {.inline.} =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why doesn't the macro version suffice?

Copy link
Member Author

@timotheecour timotheecour Aug 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the discussion of that was here: #8531 (comment)
@mratsim had suggested it, and I added it because I agreed with him it made sense:
instead of (type(a).disctinctBase)(a), you would just use a.distinctBase instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove it, all this bloat everywhere...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sigh... done.

@timotheecour
Copy link
Member Author

PTAL

of nnkEmpty, nnkLiterals:
result = node
else:
var rTree = node.kind.newTree()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use result here. That's why it exists.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done (thanks!)

@@ -198,3 +198,42 @@ macro dump*(x: typed): untyped =
let r = quote do:
debugEcho `s`, " = ", `x`
return r

# TODO: consider exporting this in macros.nim
proc replaceNodes(ast: NimNode): NimNode =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be called freshIdentNodes or similar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

and fixed distinctBase with replaceNodes; simplified a bit
implementation
;

* distinctBase now requires T to be distinct (even being recursive)
* fixed a bug in (now-default) recursive distinctBase that would only trigger on complex
stress test in tsugar.nim
@timotheecour
Copy link
Member Author

@Araq PTAL, tests completed and all comments addressed

@timotheecour
Copy link
Member Author

@mratsim since this is being revived here #13274 (comment) I was just testing and:

Unfortunately as-is undistinct doesn't pass my "type system stress test" which combines macro, static and generics.

this seems to have disappeared bw 0.19.6 and 0.20.2, so that freshIdentNodes (that you originally suggested as replaceNodes in #8531 (comment) ) doesn't seem needed anymore after that; not only that but freshIdentNodes was actually causing #13031 (comment) IIUC.

so the magic replacement introduced in #13031 may not be needed anymore and could potentially be replaced by a macro (the original one I had minus freshIdentNodes)

@timotheecour timotheecour added the TODO: followup needed remove tag once fixed or tracked elsewhere label Jul 30, 2021
@timotheecour
Copy link
Member Author

we should add a flag to make it non-recursive as option

refs
https://discord.com/channels/371759389889003530/371759389889003532/870793498264236042

@metagn
Copy link
Collaborator

metagn commented Jan 4, 2024

we should add a flag to make it non-recursive as option

Assuming this is talking about distinctBase, it does, removing followup label

@metagn metagn removed the TODO: followup needed remove tag once fixed or tracked elsewhere label Jan 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants