summary refs log tree commit diff stats
path: root/lib/js/jsffi.nim
blob: 937e3727badb9698494f63dd5a7cda3e8db48fcc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# To test stack overflow message

proc over(a: int): int = 
  if a == high(int): return
  result = over(a+1)+5
  
Echo($over(0))
f='#n239'>239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
#
#
#            Nim's Runtime Library
#        (c) Copyright 2017 Nim Authors
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This Module implements types and macros to facilitate the wrapping of, and
## interaction with JavaScript libraries. Using the provided types `JsObject`
## and `JsAssoc` together with the provided macros allows for smoother
## interfacing with JavaScript, allowing for example quick and easy imports of
## JavaScript variables:

runnableExamples:
  # Here, we are using jQuery for just a few calls and do not want to wrap the
  # whole library:

  # import the document object and the console
  var document {.importc, nodecl.}: JsObject
  var console {.importc, nodecl.}: JsObject
  # import the "$" function
  proc jq(selector: JsObject): JsObject {.importjs: "$$(#)".}

  # Use jQuery to make the following code run, after the document is ready.
  # This uses an experimental `.()` operator for `JsObject`, to emit
  # JavaScript calls, when no corresponding proc exists for `JsObject`.
  proc main =
    jq(document).ready(proc() =
      console.log("Hello JavaScript!")
    )


when not defined(js) and not defined(nimsuggest):
  {.fatal: "Module jsFFI is designed to be used with the JavaScript backend.".}

import std/[macros, tables]

const
  setImpl = "#[#] = #"
  getImpl = "#[#]"

var
  mangledNames {.compileTime.} = initTable[string, string]()
  nameCounter {.compileTime.} = 0

proc validJsName(name: string): bool =
  result = true
  const reservedWords = ["break", "case", "catch", "class", "const", "continue",
    "debugger", "default", "delete", "do", "else", "export", "extends",
    "finally", "for", "function", "if", "import", "in", "instanceof", "new",
    "return", "super", "switch", "this", "throw", "try", "typeof", "var",
    "void", "while", "with", "yield", "enum", "implements", "interface",
    "let", "package", "private", "protected", "public", "static", "await",
    "abstract", "boolean", "byte", "char", "double", "final", "float", "goto",
    "int", "long", "native", "short", "synchronized", "throws", "transient",
    "volatile", "null", "true", "false"]
  case name
  of reservedWords: return false
  else: discard
  if name[0] notin {'A'..'Z','a'..'z','_','$'}: return false
  for chr in name:
    if chr notin {'A'..'Z','a'..'z','_','$','0'..'9'}:
      return false

template mangleJsName(name: cstring): cstring =
  inc nameCounter
  "mangledName" & $nameCounter

# only values that can be mapped 1 to 1 with cstring should be keys: they have an injective function with cstring

proc toJsKey*[T: SomeInteger](text: cstring, t: type T): T {.importjs: "parseInt(#)".}

proc toJsKey*[T: enum](text: cstring, t: type T): T =
  T(text.toJsKey(int))

proc toJsKey*(text: cstring, t: type cstring): cstring =
  text

proc toJsKey*[T: SomeFloat](text: cstring, t: type T): T {.importjs: "parseFloat(#)".}

type
  JsKey* = concept a, type T
    cstring.toJsKey(T) is T

  JsObject* = ref object of JsRoot
    ## Dynamically typed wrapper around a JavaScript object.
  JsAssoc*[K: JsKey, V] = ref object of JsRoot
    ## Statically typed wrapper around a JavaScript object.

  js* = JsObject

var
  jsArguments* {.importc: "arguments", nodecl}: JsObject
    ## JavaScript's arguments pseudo-variable.
  jsNull* {.importc: "null", nodecl.}: JsObject
    ## JavaScript's null literal.
  jsUndefined* {.importc: "undefined", nodecl.}: JsObject
    ## JavaScript's undefined literal.
  jsDirname* {.importc: "__dirname", nodecl.}: cstring
    ## JavaScript's __dirname pseudo-variable.
  jsFilename* {.importc: "__filename", nodecl.}: cstring
    ## JavaScript's __filename pseudo-variable.

proc isNull*[T](x: T): bool {.noSideEffect, importjs: "(# === null)".}
  ## Checks if a value is exactly null.

proc isUndefined*[T](x: T): bool {.noSideEffect, importjs: "(# === undefined)".}
  ## Checks if a value is exactly undefined.

# Exceptions
type
  JsError* {.importc: "Error".} = object of JsRoot
    message*: cstring
  JsEvalError* {.importc: "EvalError".} = object of JsError
  JsRangeError* {.importc: "RangeError".} = object of JsError
  JsReferenceError* {.importc: "ReferenceError".} = object of JsError
  JsSyntaxError* {.importc: "SyntaxError".} = object of JsError
  JsTypeError* {.importc: "TypeError".} = object of JsError
  JsURIError* {.importc: "URIError".} = object of JsError

# New
proc newJsObject*: JsObject {.importjs: "{@}".}
  ## Creates a new empty JsObject.

proc newJsAssoc*[K: JsKey, V]: JsAssoc[K, V] {.importjs: "{@}".}
  ## Creates a new empty JsAssoc with key type `K` and value type `V`.

# Checks
proc hasOwnProperty*(x: JsObject, prop: cstring): bool
  {.importjs: "#.hasOwnProperty(#)".}
  ## Checks, whether `x` has a property of name `prop`.

proc jsTypeOf*(x: JsObject): cstring {.importjs: "typeof(#)".}
  ## Returns the name of the JsObject's JavaScript type as a cstring.

proc jsNew*(x: auto): JsObject {.importjs: "(new #)".}
  ## Turns a regular function call into an invocation of the
  ## JavaScript's `new` operator.

proc jsDelete*(x: auto): JsObject {.importjs: "(delete #)".}
  ## JavaScript's `delete` operator.

proc require*(module: cstring): JsObject {.importc.}
  ## JavaScript's `require` function.

# Conversion to and from JsObject
proc to*(x: JsObject, T: typedesc): T {.importjs: "(#)".}
  ## Converts a JsObject `x` to type `T`.

proc toJs*[T](val: T): JsObject {.importjs: "(#)".}
  ## Converts a value of any type to type JsObject.

template toJs*(s: string): JsObject = cstring(s).toJs

macro jsFromAst*(n: untyped): untyped =
  result = n
  if n.kind == nnkStmtList:
    result = newProc(procType = nnkDo, body = result)
  return quote: toJs(`result`)

proc `&`*(a, b: cstring): cstring {.importjs: "(# + #)".}
  ## Concatenation operator for JavaScript strings.

proc `+`  *(x, y: JsObject): JsObject {.importjs: "(# + #)".}
proc `-`  *(x, y: JsObject): JsObject {.importjs: "(# - #)".}
proc `*`  *(x, y: JsObject): JsObject {.importjs: "(# * #)".}
proc `/`  *(x, y: JsObject): JsObject {.importjs: "(# / #)".}
proc `%`  *(x, y: JsObject): JsObject {.importjs: "(# % #)".}
proc `+=` *(x, y: JsObject): JsObject {.importjs: "(# += #)", discardable.}
proc `-=` *(x, y: JsObject): JsObject {.importjs: "(# -= #)", discardable.}
proc `*=` *(x, y: JsObject): JsObject {.importjs: "(# *= #)", discardable.}
proc `/=` *(x, y: JsObject): JsObject {.importjs: "(# /= #)", discardable.}
proc `%=` *(x, y: JsObject): JsObject {.importjs: "(# %= #)", discardable.}
proc `++` *(x:    JsObject): JsObject {.importjs: "(++#)".}
proc `--` *(x:    JsObject): JsObject {.importjs: "(--#)".}
proc `>`  *(x, y: JsObject): JsObject {.importjs: "(# > #)".}
proc `<`  *(x, y: JsObject): JsObject {.importjs: "(# < #)".}
proc `>=` *(x, y: JsObject): JsObject {.importjs: "(# >= #)".}
proc `<=` *(x, y: JsObject): JsObject {.importjs: "(# <= #)".}
proc `**` *(x, y: JsObject): JsObject {.importjs: "((#) ** #)".}
  # (#) needed, refs https://github.com/nim-lang/Nim/pull/16409#issuecomment-760550812
proc `and`*(x, y: JsObject): JsObject {.importjs: "(# && #)".}
proc `or` *(x, y: JsObject): JsObject {.importjs: "(# || #)".}
proc `not`*(x:    JsObject): JsObject {.importjs: "(!#)".}
proc `in` *(x, y: JsObject): JsObject {.importjs: "(# in #)".}

proc `[]`*(obj: JsObject, field: cstring): JsObject {.importjs: getImpl.}
  ## Returns the value of a property of name `field` from a JsObject `obj`.

proc `[]`*(obj: JsObject, field: int): JsObject {.importjs: getImpl.}
  ## Returns the value of a property of name `field` from a JsObject `obj`.

proc `[]=`*[T](obj: JsObject, field: cstring, val: T) {.importjs: setImpl.}
  ## Sets the value of a property of name `field` in a JsObject `obj` to `v`.

proc `[]=`*[T](obj: JsObject, field: int, val: T) {.importjs: setImpl.}
  ## Sets the value of a property of name `field` in a JsObject `obj` to `v`.

proc `[]`*[K: JsKey, V](obj: JsAssoc[K, V], field: K): V
  {.importjs: getImpl.}
  ## Returns the value of a property of name `field` from a JsAssoc `obj`.

proc `[]=`*[K: JsKey, V](obj: JsAssoc[K, V], field: K, val: V)
  {.importjs: setImpl.}
  ## Sets the value of a property of name `field` in a JsAssoc `obj` to `v`.

proc `[]`*[V](obj: JsAssoc[cstring, V], field: string): V =
  obj[cstring(field)]

proc `[]=`*[V](obj: JsAssoc[cstring, V], field: string, val: V) =
  obj[cstring(field)] = val

proc `==`*(x, y: JsRoot): bool {.importjs: "(# === #)".}
  ## Compares two JsObjects or JsAssocs. Be careful though, as this is comparison
  ## like in JavaScript, so if your JsObjects are in fact JavaScript Objects,
  ## and not strings or numbers, this is a *comparison of references*.

{.experimental.}
macro `.`*(obj: JsObject, field: untyped): JsObject =
  ## Experimental dot accessor (get) for type JsObject.
  ## Returns the value of a property of name `field` from a JsObject `x`.
  runnableExamples:
    let obj = newJsObject()
    obj.a = 20
    assert obj.a.to(int) == 20
  if validJsName($field):
    let importString = "#." & $field
    result = quote do:
      proc helper(o: JsObject): JsObject
        {.importjs: `importString`, gensym.}
      helper(`obj`)
  else:
    if not mangledNames.hasKey($field):
      mangledNames[$field] = $mangleJsName($field)
    let importString = "#." & mangledNames[$field]
    result = quote do:
      proc helper(o: JsObject): JsObject
        {.importjs: `importString`, gensym.}
      helper(`obj`)

macro `.=`*(obj: JsObject, field, value: untyped): untyped =
  ## Experimental dot accessor (set) for type JsObject.
  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
  if validJsName($field):
    let importString = "#." & $field & " = #"
    result = quote do:
      proc helper(o: JsObject, v: auto)
        {.importjs: `importString`, gensym.}
      helper(`obj`, `value`)
  else:
    if not mangledNames.hasKey($field):
      mangledNames[$field] = $mangleJsName($field)
    let importString = "#." & mangledNames[$field] & " = #"
    result = quote do:
      proc helper(o: JsObject, v: auto)
        {.importjs: `importString`, gensym.}
      helper(`obj`, `value`)

macro `.()`*(obj: JsObject,
             field: untyped,
             args: varargs[JsObject, jsFromAst]): JsObject =
  ## Experimental "method call" operator for type JsObject.
  ## Takes the name of a method of the JavaScript object (`field`) and calls
  ## it with `args` as arguments, returning a JsObject (which may be discarded,
  ## and may be `undefined`, if the method does not return anything,
  ## so be careful when using this.)
  ##
  ## Example:
  ##
  ## .. code-block:: nim
  ##
  ##  # Let's get back to the console example:
  ##  var console {.importc, nodecl.}: JsObject
  ##  let res = console.log("I return undefined!")
  ##  console.log(res) # This prints undefined, as console.log always returns
  ##                   # undefined. Thus one has to be careful, when using
  ##                   # JsObject calls.
  var importString: string
  if validJsName($field):
    importString = "#." & $field & "(@)"
  else:
    if not mangledNames.hasKey($field):
      mangledNames[$field] = $mangleJsName($field)
    importString = "#." & mangledNames[$field] & "(@)"
  result = quote:
    proc helper(o: JsObject): JsObject
      {.importjs: `importString`, gensym, discardable.}
    helper(`obj`)
  for idx in 0 ..< args.len:
    let paramName = newIdentNode("param" & $idx)
    result[0][3].add newIdentDefs(paramName, newIdentNode("JsObject"))
    result[1].add args[idx].copyNimTree

macro `.`*[K: cstring, V](obj: JsAssoc[K, V],
                                   field: untyped): V =
  ## Experimental dot accessor (get) for type JsAssoc.
  ## Returns the value of a property of name `field` from a JsObject `x`.
  var importString: string
  if validJsName($field):
    importString = "#." & $field
  else:
    if not mangledNames.hasKey($field):
      mangledNames[$field] = $mangleJsName($field)
    importString = "#." & mangledNames[$field]
  result = quote do:
    proc helper(o: type(`obj`)): `obj`.V
      {.importjs: `importString`, gensym.}
    helper(`obj`)

macro `.=`*[K: cstring, V](obj: JsAssoc[K, V],
                                    field: untyped,
                                    value: V): untyped =
  ## Experimental dot accessor (set) for type JsAssoc.
  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
  var importString: string
  if validJsName($field):
    importString = "#." & $field & " = #"
  else:
    if not mangledNames.hasKey($field):
      mangledNames[$field] = $mangleJsName($field)
    importString = "#." & mangledNames[$field] & " = #"
  result = quote do:
    proc helper(o: type(`obj`), v: `obj`.V)
      {.importjs: `importString`, gensym.}
    helper(`obj`, `value`)

macro `.()`*[K: cstring, V: proc](obj: JsAssoc[K, V],
                                           field: untyped,
                                           args: varargs[untyped]): auto =
  ## Experimental "method call" operator for type JsAssoc.
  ## Takes the name of a method of the JavaScript object (`field`) and calls
  ## it with `args` as arguments. Here, everything is typechecked, so you do not
  ## have to worry about `undefined` return values.
  let dotOp = bindSym"."
  result = quote do:
    (`dotOp`(`obj`, `field`))()
  for elem in args:
    result.add elem

# Iterators:

iterator pairs*(obj: JsObject): (cstring, JsObject) =
  ## Yields tuples of type `(cstring, JsObject)`, with the first entry
  ## being the `name` of a fields in the JsObject and the second being its
  ## value wrapped into a JsObject.
  var k: cstring
  var v: JsObject
  {.emit: "for (var `k` in `obj`) {".}
  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) continue;".}
  {.emit: "  `v`=`obj`[`k`];".}
  yield (k, v)
  {.emit: "}".}

iterator items*(obj: JsObject): JsObject =
  ## Yields the `values` of each field in a JsObject, wrapped into a JsObject.
  var v: JsObject
  {.emit: "for (var k in `obj`) {".}
  {.emit: "  if (!`obj`.hasOwnProperty(k)) continue;".}
  {.emit: "  `v`=`obj`[k];".}
  yield v
  {.emit: "}".}

iterator keys*(obj: JsObject): cstring =
  ## Yields the `names` of each field in a JsObject.
  var k: cstring
  {.emit: "for (var `k` in `obj`) {".}
  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) continue;".}
  yield k
  {.emit: "}".}

iterator pairs*[K: JsKey, V](assoc: JsAssoc[K, V]): (K,V) =
  ## Yields tuples of type `(K, V)`, with the first entry
  ## being a `key` in the JsAssoc and the second being its corresponding value.
  var k: cstring
  var v: V
  {.emit: "for (var `k` in `assoc`) {".}
  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) continue;".}
  {.emit: "  `v`=`assoc`[`k`];".}
  yield (k.toJsKey(K), v)
  {.emit: "}".}

iterator items*[K, V](assoc: JsAssoc[K, V]): V =
  ## Yields the `values` in a JsAssoc.
  var v: V
  {.emit: "for (var k in `assoc`) {".}
  {.emit: "  if (!`assoc`.hasOwnProperty(k)) continue;".}
  {.emit: "  `v`=`assoc`[k];".}
  yield v
  {.emit: "}".}

iterator keys*[K: JsKey, V](assoc: JsAssoc[K, V]): K =
  ## Yields the `keys` in a JsAssoc.
  var k: cstring
  {.emit: "for (var `k` in `assoc`) {".}
  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) continue;".}
  yield k.toJsKey(K)
  {.emit: "}".}

# Literal generation

macro `{}`*(typ: typedesc, xs: varargs[untyped]): auto =
  ## Takes a `typedesc` as its first argument, and a series of expressions of
  ## type `key: value`, and returns a value of the specified type with each
  ## field `key` set to `value`, as specified in the arguments of `{}`.
  ##
  ## Example:
  ##
  ## .. code-block:: nim
  ##
  ##  # Let's say we have a type with a ton of fields, where some fields do not
  ##  # need to be set, and we do not want those fields to be set to `nil`:
  ##  type
  ##    ExtremelyHugeType = ref object
  ##      a, b, c, d, e, f, g: int
  ##      h, i, j, k, l: cstring
  ##      # And even more fields ...
  ##
  ##  let obj = ExtremelyHugeType{ a: 1, k: "foo".cstring, d: 42 }
  ##
  ##  # This generates roughly the same JavaScript as:
  ##  {.emit: "var obj = {a: 1, k: "foo", d: 42};".}
  ##
  let a = ident"a"
  var body = quote do:
    var `a` {.noinit.}: `typ`
    {.emit: "`a` = {};".}
  for x in xs.children:
    if x.kind == nnkExprColonExpr:
      let
        k = x[0]
        kString = quote do:
          when compiles($`k`): $`k` else: "invalid"
        v = x[1]
      body.add quote do:
        when compiles(`a`.`k`):
          `a`.`k` = `v`
        elif compiles(`a`[`k`]):
          `a`[`k`] = `v`
        else:
          `a`[`kString`] = `v`

    else:
      error("Expression `" & $x.toStrLit & "` not allowed in `{}` macro")

  body.add quote do:
    return `a`

  result = quote do:
    proc inner(): `typ` {.gensym.} =
      `body`
    inner()

# Macro to build a lambda using JavaScript's `this`
# from a proc, `this` being the first argument.

proc replaceSyms(n: NimNode): NimNode =
  if n.kind == nnkSym:
    result = newIdentNode($n)
  else:
    result = n
    for i in 0..<n.len:
      result[i] = replaceSyms(n[i])

macro bindMethod*(procedure: typed): auto =
  ## Takes the name of a procedure and wraps it into a lambda missing the first
  ## argument, which passes the JavaScript builtin `this` as the first
  ## argument to the procedure. Returns the resulting lambda.
  ##
  ## Example:
  ##
  ## We want to generate roughly this JavaScript:
  ##
  ## .. code-block:: js
  ##  var obj = {a: 10};
  ##  obj.someMethod = function() {
  ##    return this.a + 42;
  ##  };
  ##
  ## We can achieve this using the `bindMethod` macro:
  ##
  ## .. code-block:: nim
  ##  let obj = JsObject{ a: 10 }
  ##  proc someMethodImpl(that: JsObject): int =
  ##    that.a.to(int) + 42
  ##  obj.someMethod = bindMethod someMethodImpl
  ##
  ##  # Alternatively:
  ##  obj.someMethod = bindMethod
  ##    proc(that: JsObject): int = that.a.to(int) + 42
  if not (procedure.kind == nnkSym or procedure.kind == nnkLambda):
    error("Argument has to be a proc or a symbol corresponding to a proc.")
  var
    rawProc = if procedure.kind == nnkSym:
        getImpl(procedure)
      else:
        procedure
    args = rawProc[3].copyNimTree.replaceSyms
    thisType = args[1][1]
    params = newNimNode(nnkFormalParams).add(args[0])
    body = newNimNode(nnkLambda)
    this = newIdentNode("this")
    # construct the `this` parameter:
    thisQuote = quote do:
      var `this` {.nodecl, importc: "this".}: `thisType`
    call = newNimNode(nnkCall).add(rawProc[0], thisQuote[0][0][0])
  # construct the procedure call inside the method
  if args.len > 2:
    for idx in 2..args.len-1:
      params.add(args[idx])
      call.add(args[idx][0])
  body.add(newNimNode(nnkEmpty),
      rawProc[1],
      rawProc[2],
      params,
      rawProc[4],
      rawProc[5],
      newTree(nnkStmtList, thisQuote, call)
  )
  result = body