Skip to content

Conversation

cooldome
Copy link
Member

@cooldome cooldome commented Dec 29, 2017

One more implementation of superb feature that we can't live without ;)
Previously attempted by yglukhov: #6070 and #6138

I hope this time we will get it done. I have addressed review comments in #6138

@yglukhov
Copy link
Member

I'm really happy there's not only me who wants to implement it :D. However, there was a problem I hit in my impl. The problem is the custom pragmas are stripped away from object field definitions on the semantic phase, so you can't get those pragmas later with getType*. That's why I've suspended my impl. In order to get this right we have to leave those pragmas in the types, and fix the code that expects stripped pragmas in a few places.

@cooldome
Copy link
Member Author

Hi yglukhov, could you please describe the problem in more details (example?). Yes, pragmas are not in types but they are in nodes. They can be retrieved with getTypeInst at least, so I don't see a problem here.

@cooldome
Copy link
Member Author

yglukhov, I have extra test compared to your implementation for inner fields. getCustomPragmaVal(s.field.c, serializationKey)

Is this what you mean?

@yglukhov
Copy link
Member

@cooldome

in more details (example?)

import macros
template serializationKey(k: string) {.pragma.}

type Foo = object
    b1 {.serializationKey: "hi".}: int64

macro test(a: typed): untyped =
    echo treeRepr(getTypeImpl(a)[1].getTypeImpl())

test(Foo)

Outputs:

ObjectTy
  Empty
  Empty
  RecList
    IdentDefs
      Sym "b1"
      Sym "int64"
      Empty

I can't see a way to get the pragmas from within the macro. Or am i missing anything?

@yglukhov yglukhov mentioned this pull request Dec 29, 2017
@cooldome
Copy link
Member Author

cooldome commented Dec 29, 2017

@yglukhov
Hi Yuriy,
In the following way it works:

import macros
template serializationKey(k: string) {.pragma.}

type
  SubFoo = object
    c1 {.serializationKey: "myc1".}: int64
  Foo = object
    f1: SubFoo
    b1 {.serializationKey: "myb1".}: int64

macro test(a: typed): untyped =
  let asym = a.symbol.getImpl
  echo asym.treeRepr
  echo "---"
  let f1field = asym[2][2].findChild(it[0].kind == nnkIdent and $it[0] == "f1")
  echo f1field[1].symbol.getImpl.treeRepr

test(Foo)

outputs:

TypeDef
  Sym "Foo"
  Empty
  ObjectTy
    Empty
    Empty
    RecList
      IdentDefs
        Ident ident"f1"
        Sym "SubFoo"
        Empty
      IdentDefs
        PragmaExpr
          Ident ident"b1"
          Pragma
            ExprColonExpr
              Sym "serializationKey"
              StrLit myb1
        Sym "int64"
        Empty
---
TypeDef
  Sym "SubFoo"
  Empty
  ObjectTy
    Empty
    Empty
    RecList
      IdentDefs
        PragmaExpr
          Ident ident"c1"
          Pragma
            ExprColonExpr
              Sym "serializationKey"
              StrLit myc1
        Sym "int64"
        Empty

You can access type def pragmas using n.symbol.getImpl() and also recurse into the fields implementations if required. It does the job, however I understand the furstration as likely all of existing serialization code is using getType() and getTypeImpl() at the moment.

I have considered patching mapTypeToAstX in the compiler to produce pragmas for getTypeImpl(), but I don't think it is a good idea as it will be breaking change and every user of getTypeImpl() will be effected.

@yglukhov
Copy link
Member

yglukhov commented Dec 29, 2017

You can access type def pragmas...

That is an option I could bear with.

it will be breaking change and every user of getTypeImpl() will be effected.

Now regarding the getTypeImpl breakage, I suppose it will break only for those types with custom pragmas, which currently do not exist, so I would not call that a breakage at all. Besides, Nim is getting close to 1.0 so it is the perfect time to correct things now. Breakage after 1.0 will be a lot harder to advocate.

compiler/ast.nim Outdated
mNHint, mNWarning, mNError,
mInstantiationInfo, mGetTypeInfo, mNGenSym,
mNimvm, mIntDefine, mStrDefine, mRunnableExamples,
mCustomPragma,
Copy link
Member

Choose a reason for hiding this comment

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

Dead code.

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, removed

compiler/ast.nim Outdated
sfOverriden, # proc is overriden
sfGenSym # symbol is 'gensym'ed; do not add to symbol table
sfGenSym, # symbol is 'gensym'ed; do not add to symbol table,
sfCustomPragma # symbol is custom pragma template
Copy link
Member

Choose a reason for hiding this comment

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

I would introduce an alias to an existing sfFlag here to keep the number of flags below 32.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to the alias of existing sfFlag. As alternative I can make a new SymKind, I know it is a bit of work, but if you find it to be better solution I can do that.

Copy link
Member

Choose a reason for hiding this comment

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

No new SymKind. Why? Because the symbol kinds should reflect the concrete syntax, the syntax is template foo {.pragma.} and so it should be an skTemplate with an sfPragma flag.

@cooldome
Copy link
Member Author

Hi Araq, I need this feature for my project in the next two weeks.
Tell me what is required to make this feature production ready for the upcoming release, I'll do my best to meet the aggressive timeline.

Help is very much appreciated.

elif n.kind == nkExprColonExpr and n[1].kind == nkBracket:
# pragma: [arg1, arg2] -> pragma(arg1, arg2)
result.add n[0]
result.sons &= n[1].sons
Copy link
Member

Choose a reason for hiding this comment

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

I don't like direct 'sons' accesses but have no better solution right now.

Copy link
Member

Choose a reason for hiding this comment

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

Now I have: Write a helper proc in ast.nim that does the required transformations.

@Araq
Copy link
Member

Araq commented Dec 31, 2017

The feature's implementation looks fine but it needs documentation. Please update the manual.

@cooldome
Copy link
Member Author

cooldome commented Jan 2, 2018

Thanks Araq, I have updated the manual and added ast helpers for sons manipulation

-------------------
It is possible to define custom typed pragmas. Custom pragmas do not effect
code generation directly, but their presence can be detected by macros.
Custom pragmas are defined using ``pragma`` keyword:
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick, but pragma isn't a keyword? Should be: "/.../ defined using the pragma pragma".

Great to see this feature implemented!

Copy link
Member Author

Choose a reason for hiding this comment

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

Make sense, updated the doc

In this example custom pragmas are used to describe how Nim objects are
mapped to the schema of the relational database. Custom pragmas can have
zero, one or more arguments. In order to pass multiple arguments enclose
them into brackets `[]` as for the rest of the pragmas. If you need to pass
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be done with () instead, other pragmas already use tuple syntax and it makes more sense than the array syntax.

Copy link
Member Author

@cooldome cooldome Jan 3, 2018

Choose a reason for hiding this comment

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

I have done it in the latest commit, however it required a minor parser change to make pass by name work. I was getting nkAsgn nodes instead of nkExprEqExpr. I hope your are ok with it

mapped to the schema of the relational database. Custom pragmas can have
zero, one or more arguments. In order to pass multiple arguments enclose
them into brackets `[]` as for the rest of the pragmas. If you need to pass
a single argument which is a const array, double brackets will be required.
Copy link
Member

Choose a reason for hiding this comment

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

See? That's not good.

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 like () even more, I thought I have to comply with existing pragmas syntax. Good we have changed it to ()


if n.kind == nkIdent:
result = result[0]
elif n.kind == nkExprColonExpr and n[1].kind == nkBracket:
Copy link
Member

Choose a reason for hiding this comment

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

n[1].kind == nkPar

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


type
User {.dbTable: ("users", tblspace).} = object
id {.dbKey: (primary_key = true).}: int
Copy link
Member

Choose a reason for hiding this comment

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

Ugh, no. Tuple constructors use ':'!

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 kind of disagree. Yes, tuple constructors are using ':', but it is not a tuple. It is more of call expression and I am passing the argument by name.
IMO, tuple constructors don't fit as they can't have a mix of unnamed and named arguments, while proc/template calls can.

Let me know if got everything wrong.

Copy link
Member

@Araq Araq Jan 3, 2018

Choose a reason for hiding this comment

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

It's clearly a tuple that is passed to the template here. That said, I think pragmas should just allow ordinary call syntax without the colons, importc"foo" that would solve this issue. I always wanted to allow this.

Copy link
Member Author

Choose a reason for hiding this comment

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

All done now, it well worth a double shot of wiskey at FOSDEM

var y = a.sons[1]
if x.kind == nkExprColonExpr: x = x.sons[1]
if y.kind == nkExprColonExpr: y = y.sons[1]
if x.kind in nkPragmaCallKinds: x = x.sons[1]
Copy link
Member

Choose a reason for hiding this comment

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

Very good, but nkCall is not guaranteed to have 2 or more children, so you need a x.len check here too where there was none required before.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for quick review, added len checks in the latest commit. Let's see how tests will go.

@Araq
Copy link
Member

Araq commented Jan 7, 2018

Superb work. I love it.

@cooldome
Copy link
Member Author

cooldome commented Jan 7, 2018

Agree, len checks were required. Added them in the latest submit

@data-man
Copy link
Contributor

data-man commented Jan 7, 2018

Many repetitions:

if n.kind notin nkPragmaCallKinds or n.len != 2:

Maybe to write a separate proc for this check?

@cooldome
Copy link
Member Author

cooldome commented Jan 7, 2018

I don't see a problem in repetition, it generally follows compiler code convention.
Ultimately, it is Araq's call. If he wants these checks as a separate function I can do it in 20 minutes

@cooldome
Copy link
Member Author

cooldome commented Jan 8, 2018

ready for the next review


More examples with custom pragmas:
- Better serialization/deserialization control:
.. code-block:: nim
Copy link
Member

Choose a reason for hiding this comment

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

Newline before this 'code-block'.

c {.serializationKey: "_c".}: string

- Adopting type for gui inspector in a game engine:
.. code-block:: nim
Copy link
Member

Choose a reason for hiding this comment

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

Newline before this 'code-block'.

you cannot do yourself by walking AST object representation.

More examples with custom pragmas:
- Better serialization/deserialization control:
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 a heading.

b {.defaultDeserialize: 5.}: int
c {.serializationKey: "_c".}: string

- Adopting type for gui inspector in a game engine:
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 a heading.

template dbIgnore {.pragma.}

Consider stylized example of possible Object Relation Mapping (ORM) implementation:
.. code-block:: nim
Copy link
Member

Choose a reason for hiding this comment

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

Newline before this 'code-block'.

used.


User custom pragmas
Copy link
Member

Choose a reason for hiding this comment

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

The name of this feature shall be "Custom annotations"

@Araq
Copy link
Member

Araq commented Jan 8, 2018

Travis is red because of the illformed RST documentation.

------------------
It is possible to define custom typed pragmas. Custom pragmas do not effect
code generation directly, but their presence can be detected by macros.
Custom pragmas are defined using pragma ``pragma``:
Copy link
Contributor

Choose a reason for hiding this comment

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

When I read this first it seemed as if this "pragma" can only be named pragma, please explain that any arbitrary identifier is possible.

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

type MyComponent = object
position {.editable, animatable.}: Vector3
alpha {.editRange: [0.0..1.0], animatable.}: float32

Copy link
Contributor

Choose a reason for hiding this comment

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

An example implementation for one of these pragmas would be nice

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 am not really game or gui developer so I can't implement it in realistic way, I have taken this example from yglukhov first proposal as I thought the more examples the better. I can leave it as it is or remove it.

##
## .. code-block:: nim
## template myAttr() = discard
## type MyObj = object
Copy link
Contributor

Choose a reason for hiding this comment

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

convention is that type should be on its own line.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

## type MyObj = object
## myField {.serializationKey: "mf".}: int
## var o: MyObj
## assert(o.myField.customPragmaNode(serializationKey) == "mf")
Copy link
Contributor

Choose a reason for hiding this comment

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

This example uses a private proc.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

return newLit(true)
return newLit(false)

macro getCustomPragmaVal*(n: typed, cp: typed{nkSym}): untyped =
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a better API would be getCustomPragma and possibly an additional getValue procedure.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm, so you don't think it's useful to be able to retrieve the CustomPragma itself?

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 don't think getCustomPragma can be a macro, what it is going to return? Invalid NimNode.
However, getCustomPragma can be a compile time proc that is used inside other user macros to do something with pragma node. Actually, I have such proc already it is called customPragmaNode, but it is not exported.

Should I rename it and make it public?

Copy link
Member

Choose a reason for hiding this comment

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

Should I rename it and make it public?

We can do that later, I'm merging this now, I don't want to keep reviewing it.

##
## .. code-block:: nim
## template serializationKey(key: string) = discard
## type MyObj = object
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

localError(it.info, "'experimental' pragma only valid as toplevel statement")
of wThis:
if it.kind == nkExprColonExpr:
if it.kind in nkPragmaCallKinds and it.len == 2:
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @data-man here. You're using this expression many times.

But what's possibly error prone is that you're using the negation of this too (just above on line 988, and probably in other places too). It would be far clearer if this expression was in some nice proc and you negated it when necessary.

Copy link
Member

Choose a reason for hiding this comment

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

That's a bit unfair since the original code had the same issues. I planned to clean this one up after the merge. But ok, since other stuff needs to be reworked too, no big issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it was as severe previously: only the kind was checked, no?

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 disagree. Introduction of a new function for every pair of checks will turn compiler's code into hell, because it will contain 30000 one line helper procs that everybody will have to learn before reading compilers code.

It is pointless: it will not save a single line of code, because if statements can't be removed as error messages are different for different pragma.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to let @Araq decide. If this was my code though I wouldn't accept it. This is a common check.

@Araq
Copy link
Member

Araq commented Jan 9, 2018

Unrelated test failure. Merging.

@Araq Araq merged commit 2c9e56a into nim-lang:devel Jan 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants