<commit_before># Here are all of the builtin functions that verbs can call

import strutils
import tables
import sequtils
import math
import nre
import options
import times

import types
import objects
import verbs
import scripting
import persist
import compile
import tasks
import querying
import server

# for hashing builtins
import bcrypt

# Provided by bcrypt, but not exported; used for the `random` builtin
# TODO: find an alternative?
proc arc4random: int32 {.importc: "arc4random".}

proc strToType(str: string): tuple[b: bool, t: MDataType] =
  case str.toLowerAscii():
    of "int": return (true, dInt)
    of "float": return (true, dFloat)
    of "str": return (true, dStr)
    of "sym": return (true, dSym)
    of "obj": return (true, dObj)
    of "list": return (true, dList)
    of "table": return (true, dTable)
    of "err": return (true, dErr)
    of "nil": return (true, dNil)
    else: return (false, dInt)

proc typeToStr(typ: MDataType): string =
  case typ:
    of dInt:   "int"
    of dFloat: "float"
    of dStr:   "str"
    of dSym:   "sym"
    of dObj:   "obj"
    of dList:  "list"
    of dTable: "table"
    of dErr:   "err"
    of dNil:   "nil"

# turns all %s to \s because %s are easier to use within the server
proc escapeRegex(pat: string): Regex =
  re(pat.replace(re"%(.)", proc (m: RegexMatch): string =
    let capt = m.captures[0]
    if capt == "%":
      return "%"
    else:
      return "\\" & capt))

# Convenience templates: these are to be called from builtins to extract
# values from their arguments but raising an error if they're not of the
# correct data type.
template extractInt(d: MData): int =
  let dV = d
  checkType(dV, dInt)
  dV.intVal

template extractFloat(d: MData): float =
  let dV = d
  var res: float
  if dV.isType(dFloat):
    res = dV.floatVal
  elif dV.isType(dInt):
    res = dV.intVal.float
  else:
    let msg = "$#: expected argument of type int or float, instead got $#"
    runtimeError(E_TYPE, msg % [bname, typeToStr(dV.dtype)])

  res

template extractString(d: MData): string =
  let dV = d
  checkType(dV, dStr)
  dV.strVal
template extractList(d: MData): seq[MData] =
  let dV = d
  checkType(dV, dList)
  dV.listVal
template extractTable(d: MData): Table[MData, MData] =
  let dV = d
  checkType(dV, dTable)
  dV.tableVal
template extractError(d: MData): tuple[e: MError, s: string] =
  let dV = d
  checkType(dV, dErr)
  (dV.errVal, dV.errMsg)

template extractObject(objd: MData): MObject =
  let objdV = objd
  checkType(objdV, dObj)
  let obj = world.dataToObj(objdV)
  if not obj.isSome():
    runtimeError(E_ARGS, "invalid object " & $objdV)

  obj.get()

# Error-checking templates:
# Builtins need to check permissions and types. If any of these
# checks fail, then a error is thrown in the program.
template checkForError(value: MData) =
  let valueV = value
  if valueV.isType(dErr) and valueV.errVal != E_NONE:
    return valueV.pack

template runtimeError(error: MError, message: string) =
  return error.md(message).pack

template checkType(value: MData, expected: MDataType, ifnot: MError = E_TYPE) =
  let valueV = value
  let expectedV = expected
  if not valueV.isType(expectedV):
    runtimeError(ifnot,
      bname & ": expected argument of type " & typeToStr(expectedV) &
        " instead got " & typeToStr(valueV.dType))

template isWizardT: bool = isWizard(task.owner)
template owns(what: MObject): bool = task.owner.owns(what)

template checkOwn(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.owns(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " doesn't own " & whatV.toObjStr())

template checkRead(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read " & whatV.toObjStr())
template checkWrite(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write " & whatV.toObjStr())
template checkRead(what: MProperty) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read property: " & whatV.name)
template checkWrite(what: MProperty) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write property: " & whatV.name)
template checkRead(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read verb: " & whatV.names)
template checkWrite(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write verb: " & whatV.names)
template checkExecute(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canExecute(verb):
    runtimeError(E_PERM, obj.toObjStr() & " cannot execute verb: " & whatV.names)

# This is so that strings can be echoed without the quotes surrounding them.
proc toEchoString*(x: MData): string =
  if x.isType(dStr):
    x.strVal
  else:
    $x

## ::
##
##   (echo arg1:Any ...):List
##
## Takes any number of arguments of any type and outputs them to object on
## which the verb is being run on. Each argument is passed to the
## `toEchoString` proc before being output.
##
## ::
##
##   (let ((obj #5))
##     (echo "object " obj " is an object"))
##
##   ;;; This outputs "object #5 is an object" to self
##
## The argument list is simply returned.
##
## NOTE: This is probably not what you want to call because you want to echo to
## the player who typed the verb. If this builtin is called from a verb running
## on some object that isn't a player, nobody will see he message. (look into
## the ``notify`` builtin)
defBuiltin "echo":
  var
    newArgs: seq[MData] = @[]
    sendstr = ""
  for arg in args:
    let res = arg
    sendstr &= res.toEchoString()
    newArgs.add(res)
  self.send(sendstr)
  return newArgs.md.pack

## ::
##
##   (notify player:Obj message:Str):Str
##
## Sends a line of output ``message`` to ``player``. There are some
## restrictions, however. The programmer who's permissions this task running
## with must either own ``player`` or be a wizard. If one of these conditions
## isn't met, `E_PERM` is raised.
##
## The value returned is the string that was printed.
defBuiltin "notify":
  if args.len != 2:
    runtimeError(E_ARGS, "notify takes 2 arguments")

  let who = extractObject(args[0])
  let msg = extractString(args[1])

  if task.owner != who and not isWizardT():
    runtimeError(E_PERM, "$# cannot notify $#" % [$owner, $who])

  who.send(msg)
  return msg.md.pack

## ::
##
##   (do expr1:Any expr2:Any ...):Any
##
## This builtin evaluates each expression passed to it in order and returns the
## result of the last one evaluated. This is one way to do multiple things in a
## lambda:
##
## ::
##
##   (lambda (x)
##     (do
##      (mangle x)
##      (eat x)
##      (clean-up)))
defBuiltin "do":
  var newArgs: seq[MData] = @[]
  for arg in args:
    let res = arg
    newArgs.add(res)

  if newArgs.len > 0:
    return newArgs[^1].pack
  else:
    return @[].md.pack

## ::
##
##   (parse code:String):Any
##
## Parses ``code`` and returns the object that it was

defBuiltin "parse":
  if args.len != 1:
    runtimeError(E_ARGS, "parse takes 1 argument")

  let code = extractString(args[0])

  var parser = newParser(code)
  let parsed = parser.parseAtom()
  checkForError(parser.error)

  return parsed.pack

## ::
##
##   (eval form:Any):Any
##
## Treats ``form`` as actual code, evaluates it, and returns the result.
## Internally, the code is compiled and loaded into a whole new task which is
## then run. **Use sparingly**.
defBuiltin "eval":
  if phase == 0:
    if args.len != 1:
      runtimeError(E_ARGS, "eval takes 1 argument")

    let form = args[0]

    # Design choice: the compiler runs with the permissions of THE PLAYER WHO TYPED THE COMMAND.
    # This is used for executing compile-time code.
    let instructions = compileCode(form, player)
    checkForError(instructions.error)

    discard world.addTask("eval", self, player, caller, owner, symtable, instructions,
                        taskType = task.taskType, callback = task.id)
    task.setStatus(tsAwaitingResult)
    return 1.pack
  if phase == 1:
    return args[1].pack

## ::
##
##   (settaskperms new-perms:Obj):Obj
##
## Sets the current task's permissions to those of ``new-perms`` (which is also
## returned). The main use of this is the idiom:
##
## ::
##
##   (setttaskperms caller)
##
## To make sure that a verb is no longer running with wizard permissions and
## doesn't royally screw things up.
defBuiltin "settaskperms":
  if args.len != 1:
    runtimeError(E_ARGS, "settaskperms takes 1 argument")

  let newPerms = extractObject(args[0])
  if not isWizardT() and task.owner != newPerms:
    runtimeError(E_PERM, "$# can't set task perms to $#" %
                  [task.owner.toObjStr(), newPerms.toObjStr()])

  task.owner = newPerms
  return newPerms.md.pack

## ::
##
##   (callerperms):Obj
##
## Returns the object whose permissions the current task is running
## with.
defBuiltin "callerperms":
  if args.len != 0:
    runtimeError(E_ARGS, "callerperms takes no arguments")

  return task.owner.md.pack

## ::
##
##   (read [player:Obj]):Str
##
## Reads a line of input from ``player``'s connection (if omitted it reads
## from the player who entered the command that started this task).
##
## The programmer must own ``player`` or be a wizard or ``E_PERM`` is raised.
##
## **Use extremely sparingly**.
defBuiltin "read":
  if phase == 0:
    var who: MObject
    case args.len:
      of 0:
        who = player
      of 1:
        who = extractObject(args[0])
      else:
        runtimeError(E_ARGS, "read takes 0 or 1 arguments")

    if not isWizardT() and who != caller:
      runtimeError(E_PERM, "you don't have permission to read from that connection")

    let client = findClient(who)
    if isNil(client):
      runtimeError(E_ARGS, who.toObjStr() & " has not been connected to!")

    task.askForInput(client)
    return 1.inputPack
  elif phase == 1:
    # sanity check
    if args.len != 1:
      runtimeError(E_ARGS, "read failed")

    return args[0].pack

## ::
##
##   (err error-type:Err message:Str)
##
## Raises an error with type ``error-type`` and message ``message``.
##
## Example of use::
##
##   (if (> count 10)
##       (err E_ARGS "too many")
##       (do-something-with count))
defBuiltin "err":
  if args.len != 2:
    runtimeError(E_ARGS, "err takes 2 arguments")

  var err = args[0]
  checkType(err, dErr)

  let msg = extractString(args[1])

  err.errMsg = msg
  err.trace = @[]
  return err.pack

## ::
##
##   (erristype err:Err err2:Err):Int
##
## Checks whether ``err`` has same error type as ``err2``. If true, it returns
## ``1``, if not ``0``. This is useful in ``try`` expressions:
##
## ::
##
##   (try (error-prone-code)
##        (if (erristype error E_PERM)     ; note: in catch block, `error` holds the error
##            (echo "Permission error")
##            (echo "Some other kind of error)))
defBuiltin "erristype":
  if args.len != 2:
    runtimeError(E_ARGS, "erristype takes 2 arguments")

  var (err, _) = extractError(args[0])
  var (err2, _) = extractError(args[1])

  return if err == err2: 1.md.pack else: 0.md.pack

proc extractInfo(prop: MProperty): MData =
  var res: seq[MData] = @[]
  res.add(prop.owner.md)

  var perms = ""
  if prop.pubRead: perms &= "r"
  if prop.pubWrite: perms &= "w"
  if prop.ownerIsParent: perms &= "c"

  res.add(perms.md)
  return res.md

proc extractInfo(verb: MVerb): MData =
  var res: seq[MData] = @[]
  res.add(verb.owner.md)

  var perms = ""
  if verb.pubRead: perms &= "r"
  if verb.pubWrite: perms &= "w"
  if verb.pubExec: perms &= "x"

  res.add(perms.md)
  res.add(verb.names.md)
  return res.md

proc extractArgs(verb: MVerb): MData =
  var res: seq[MData] = @[]
  res.add(objSpecToStr(verb.doSpec).md)
  res.add(prepSpecToStr(verb.prepSpec).md)
  res.add(objSpecToStr(verb.ioSpec).md)

  return res.md

type
  PropInfo = tuple[owner: MObject, perms: string, newName: string]
  VerbInfo = tuple[owner: MObject, perms: string, newName: string]
  VerbArgs = tuple[doSpec: ObjSpec, prepSpec: PrepType, ioSpec: ObjSpec]

template propInfoFromInput(info: seq[MData]): PropInfo =
  if info.len != 2 and info.len != 3:
    runtimeError(E_ARGS, "property info must be a list of size 2 or 3")

  var res: PropInfo

  let ownerd = info[0]
  let ownero = extractObject(ownerd)
  res.owner = ownero

  let perms = extractString(info[1])
  res.perms = perms

  if info.len == 3:
    let newName = extractString(info[2])
    res.newName = newName

  res

template verbInfoFromInput(info: seq[MData]): VerbInfo =
  if info.len != 2 and info.len != 3:
    runtimeError(E_ARGS, "verb info must be a list of size 2 or 3")

  var res: VerbInfo

  let ownerd = info[0]
  let ownero = extractObject(ownerd)
  res.owner = ownero

  let perms = extractString(info[1])
  res.perms = perms

  if info.len == 3:
    let newName = extractString(info[2])
    res.newName = newName

  res

template objSpecFromData(ospd: MData): ObjSpec =
  let str = extractString(ospd)
  let ospec = strToObjSpec(str)

  if not ospec.isSome():
    runtimeError(E_ARGS, "invalid object spec '$1'" % str)

  ospec.get()

template prepSpecFromData(pspd: MData): PrepType =
  let str = extractString(pspd)
  let pspec = strToPrepSpec(str)

  if not pspec.isSome():
    runtimeError(E_ARGS, "invalid preposition spec '$1'" % str)

  pspec.get()

template verbArgsFromInput(info: seq[MData]): VerbArgs =
  if info.len != 3:
    runtimeError(E_ARGS, "verb args must be a list of size 3")

  var vargs: VerbArgs
  vargs.doSpec = objSpecFromData(info[0])
  vargs.prepSpec = prepSpecFromData(info[1])
  vargs.ioSpec = objSpecFromData(info[2])

  vargs

proc setInfo(prop: MProperty, info: PropInfo) =
  prop.owner = info.owner
  prop.pubRead = "r" in info.perms
  prop.pubWrite = "w" in info.perms
  prop.ownerIsParent = "c" in info.perms

  prop.name = info.newName

proc setInfo(verb: MVerb, info: VerbInfo) =
  verb.owner = info.owner
  verb.pubRead = "r" in info.perms
  verb.pubWrite = "w" in info.perms
  verb.pubExec = "x" in info.perms

  verb.names = info.newName

proc setArgs(verb: MVerb, args: VerbArgs) =
  verb.doSpec = args.doSpec
  verb.prepSpec = args.prepSpec
  verb.ioSpec = args.ioSpec

template getPropOn(objd, propd: MData, die = true, useDefault = false,
                   default = nilD, all = false, inherited = true):
                     tuple[o: MObject, p: MProperty] =
  let objd2 = objd
  let obj = extractObject(objd2)
  var res: tuple[o: MObject, p: MProperty]

  let
    propName = extractString(propd)
    (objOn, propObj) = obj.getPropAndObj(propName, all)

  if isNil(propObj):
    if useDefault:
      res = (nil, newProperty("default", default, nil))
    else:
      if die:
        runtimeError(E_PROPNF, "property $1 not found on $2" % [propName, $obj.toObjStr()])
      else:
        return nilD.pack
  else:
    if not all and not inherited and obj.propIsInherited(propObj):
      runtimeError(E_PROPNF, "property $1 not found on $2" % [propName, $obj.toObjStr()])

    res = (objOn, propObj)
  res

template getVerbOn(objd, verbdescd: MData, die = true,
                   all = false): tuple[o: MObject, v: MVerb] =

  let objd2 = objd
  let obj = extractObject(objd2)

  var res: tuple[o: MObject, v: MVerb]

  let verbdescd2 = verbdescd
  if verbdescd2.isType(dStr):
    let verbdesc = verbdescd2.strVal

    let (objOn, verb) = obj.getVerbAndObj(verbdesc, all)
    if isNil(verb):
      if die:
        runtimeError(E_VERBNF, "verb $1 not found on $2" % [verbdesc, obj.toObjStr()])
      else:
        return nilD.pack

    res = (objOn, verb)
  elif verbdescd2.isType(dInt):
    let verbnum = verbdescd2.intVal

    if verbnum >= obj.verbs.len or verbnum < 0:
      runtimeError(E_VERBNF, "verb index $# out of range" % $verbnum)

    res = (obj, obj.verbs[verbnum])
  else:
    runtimeError(E_ARGS, "verb indices can only be strings or integers")

  res

## ::
##
##   (getprop obj:Obj prop:Str):Any
##
## Gets the property `prop` of `obj`. If the property doesn't exist,
## ``E_PROPNF`` is raised. The programmer should also have read access on the
## object. If not, ``E_PERM`` is raised.
##
## There is a shorthand for this builtin but it must be used carefully because
## it is rudimentary:
##
## ::
##
##   (echo "The value of #5.name is " #5.name)
##
## instead of:
##
## ::
##
##   (echo "The value of #5.name is " (getprop #5 "name"))
##
## In short, ``obj.prop`` expands to ``(getpop obj "prop")``, but don't expect
## it to work with verb calls: ``obj.prop:verb()`` **will not** work and neither
## will shenanigans like ``(callerperms).name``. However, these can be nested:
## ``obj.location.name`` will work.
defBuiltin "getprop":
  var useDefault = false
  var default = nilD
  case args.len:
    of 2: discard
    of 3:
      default = args[2]
      useDefault = true
    else:
      runtimeError(E_ARGS, "getprop takes 2 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1],
                                 all = true,
                                 default = default,
                                 useDefault = useDefault)
  discard obj

  checkRead(propObj)

  return propObj.val.pack

## ::
##
##   (setprop obj:Obj prop:Str new-val:Any):Any
##
## Companion of ``getprop``. It sets the property described by ``prop`` on
## ``obj`` to ``new-val``. If ``prop`` cannot be found on ``obj`` then
## ``E_PROPNF`` is raised. If the programmer doesn't have write access on
## ``obj`` then ``E_PERM`` is raised. The value returned is the value that was
## set.
##
## There is no cute shorthand for this builtin to avoid overcomplicating the
## syntax.
defBuiltin "setprop":
  if args.len != 3:
    runtimeError(E_ARGS, "setprop takes 3 arguments")

  let objd = args[0]
  let obj = extractObject(objd)

  let newVal = args[2]

  let propName = extractString(args[1])
  var oldProp = obj.getProp(propName, all = false)

  if isNil(oldProp):
    checkWrite(obj)
    let (newProp, error) = obj.setProp(propName, newVal)
    checkForError(error)

    newProp.owner = task.owner
    world.persist(obj)
  else:
    checkWrite(oldProp)
    # Even though we could just do oldProp.val = newVal,
    # we need to call setProp. This is because there are
    # some special properties whose types are enforced.
    let (_, error) = obj.setProp(propName, newVal)
    checkForError(error)

    if propName == "owner":
      # The following line is safe because setProp would have returned an error
      # if the property "owner" was set to a non-object
      let newOwner = newVal.extractObject()
      for prop in obj.props:
        if prop.ownerIsParent:
          prop.owner = newOwner

    world.persist(obj)

  return newVal.pack

## ::
##
##   (delprop obj:Obj prop:Str):Obj
##
## This builtin is for deleting a property from an object and all its
## descendants. If the property is not found, then ``E_PROPNF`` is raised. If
## the programmer does not have write permissions on ``obj`` then ``E_PERM`` is
## raised.  The return value is ``obj``.
defBuiltin "delprop":
  if args.len != 2:
    runtimeError(E_ARGS, "delprop takes 2 arguments")

  let (obj, prop) = getPropOn(args[0], args[1], inherited = false)

  for moddedObj, deletedProp in obj.delPropRec(prop).items:
    discard deletedProp
    world.persist(moddedObj)

  return obj.md.pack

## ::
##
##   (getpropinfo obj:Obj prop:Str):List
##
## Retrieves information about the property referred to by ``prop`` on ``obj``.
## This information is a list whose first element is the owner of the property.
## The second element is a string of characters taken from the set: ``r``, ``w`,
## and ``c``. ``r`` signifies that the property is publicly readable. ``w``
## signifies that the property is publicly writable. ``c`` signifies that the
## owner of this property in this object's descendants should be the owner of
## the child, not the owner of this object.
##
## If the property doesn't exist, then ``E_PROPNF`` is raised. If the
## programmer does not have read access to ``obj`` then ``E_PERM`` is raised.
defBuiltin "getpropinfo":
  if args.len != 2:
    runtimeError(E_ARGS, "getpropinfo takes 2 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1])
  discard obj

  checkRead(propObj)

  return extractInfo(propObj).pack


## ::
##
##   (setpropinfo obj:Obj prop:Str new-info:List):Obj
##
## Sets the property ``prop`` on ``obj``'s info to ``new-info``. ``new-info``
## is specified by ``(new-owner:Obj new-perms:Str [new-name:Str])``.
## ``new-perms`` is in the same format as the part of the result of
## ``getpropinfo``.
##
## If the property does not exist, ``E_PROPNF`` is raised and if the programmer
## does not have write access to the ``obj`` then ``E_PERM`` is raised.
defBuiltin "setpropinfo":
  if args.len != 3:
    runtimeError(E_ARGS, "setpropinfo takes 3 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1])

  checkWrite(propObj)

  # validate the property info
  let
    propinfo = extractList(args[2])
    info = propInfoFromInput(propinfo)

  propObj.setInfo(info)
  world.persist(obj)


  return args[0].pack

## ::
##
##   (props obj:Obj):List
##
## Returns a list of properties defined directly on ``obj``. If the programmer
## does not have read access to ``obj``, ``E_PERM`` is raised.
defBuiltin "props":
  if args.len != 1:
    runtimeError(E_ARGS, "props takes 1 argument")

  let objd = args[0]
  let obj = extractObject(objd)

  checkRead(obj)

  let res = obj.getOwnProps().map(md)

  return res.md.pack

## ::
##
##   (verbs obj:Obj):List
##
## Returns a list of verbs defined directly on ``obj``. If the programmer does
## not have read access to ``obj``, ``E_PERM`` is raised.
defBuiltin "verbs":
  if args.len != 1:
    runtimeError(E_ARGS, "verbs takes 1 argument")

  let objd = args[0]
  let obj = extractObject(objd)

  checkRead(obj)

  var res: seq[MData] = @[]
  for v in obj.verbs:
    res.add(v.names.md)

  return res.md.pack


## ::
##
##   (getverbinfo obj:Obj verb:Str):List
##   (getverbinfo obj:Obj verb-index:Int):List
##
## See the documentation of ``getpropinfo`` for the format of the list
## returned.  The only difference is that instead of ``c`` in the permissions
## there is ``x`` which signifies if the verb is publicly executable.
##
## If ``verb`` is not found on ``obj``, then ``E_VERBNF`` is raised. If the
## programmer does not have read permissions on the object and the verb then
## ``E_PERM`` is raised.
defBuiltin "getverbinfo":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbinfo takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj
  checkRead(verb)

  return extractInfo(verb).pack

## ::
##
##   (setverbinfo obj:Obj verb:Str new-info:List):Obj
##   (setverbinfo obj:Obj verb-index:Int new-info:List):Obj
##
## This is the companion verb to ``getverbinfo``. The ``new-info`` list
## is of the same format as the return value of ``getverbinfo`` but can
## contain one extra element at the end that specifies a new list of names
## for the verb in a string.
##
## The return value is the ``obj``.
defBuiltin "setverbinfo":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbinfo takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let verbInfo = extractList(args[2])

  let info = verbInfoFromInput(verbInfo)

  verb.setInfo(info)
  world.persist(obj)
  return args[0].pack

## ::
##
##   (getverbargs obj:Obj verb:Str):List
##   (getverbargs obj:Obj verb-index:Int):List
##
## This builtin returns the arguments that the verb is supposed to operate on.
## It will be in the list of the form:
##
## ::
##
##   (direct-object preposition indirect-object)
##
## If ``verb`` does not exist on ``obj`` then ``E_VERBNF`` is raised. If the
## programmer does not have read permissions on ``verb`` then ``E_PERM`` is
## raised.
defBuiltin "getverbargs":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbargs takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj
  checkRead(verb)

  return extractArgs(verb).pack

## ::
##
##   (setverbargs obj:Obj verb:Str new-args:List):Obj
##   (setverbargs obj:Obj verb-index:Int new-args:List):Obj
##
## This is the companion verb to ``getverbargs``. ``setverbinfo`` is to
## ``getverbinfo`` as this verb is to ``getverbargs``. The signature above
## should be enough to infer how to use it. Write access is required or
## ``E_PERM`` is raised.
defBuiltin "setverbargs":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbargs takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let verbArgs = extractList(args[2])

  let argsInfo = verbArgsFromInput(verbArgs)
  verb.setArgs(argsInfo)
  world.persist(obj)

  return args[0].pack

## ::
##
##   (addverb obj:Obj new-verb-names:Str):Obj
##
## Adds a verb to ``obj`` with names specified by ``new-verb-names``. The new
## verb's owner will be the programmer of this verb. If the programmer does not
## have write permissions on ``obj``, ``E_PERM`` is raised. The return value of
## this verb is the object that gained a new verb.
defBuiltin "addverb":
  if args.len != 2:
    runtimeError(E_ARGS, "addverb takes 2 arguments")

  let objd = args[0]
  let obj = extractObject(objd)

  checkWrite(obj)

  let names = extractString(args[1])

  var verb = newVerb(
    names = names,
    owner = owner,
  )

  checkForError(verb.setCode("", owner))

  discard obj.addVerb(verb)
  world.persist(obj)

  return objd.pack

## ::
##
##   (delverb obj:Obj verb:Str):Obj
##   (delverb obj:Obj verb-index:Int):Obj
##
## Deletes the verb with name ``verb`` or index ``verb-index`` off ``obj`` and
## all of its descendants. If the string ``verb`` does not point to any verb on
## ``obj`` then ``E_VERBNF`` is raised. If the programmer doesn't have write
## access to the ``obj`` then ``E_PERM`` is raised.
##
## The return value is the ``obj``.
defBuiltin "delverb":
  if args.len != 2:
    runtimeError(E_ARGS, "delverb takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])

  checkWrite(obj)

  if isNil(verb) or verb.inherited:
    runtimeError(E_VERBNF, "$1 does not define a verb $2" % [obj.toObjStr, $args[1]])

  discard obj.delVerb(verb)
  world.persist(obj)

  return obj.md.pack


defBuiltin "loadverb":
  if args.len != 2:
    runtimeError(E_ARGS, "loadverb takes 2 arguments (object and verb name)")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let resultd = readVerbCode(world, obj, verb, player)
  checkForError(resultd)
  world.persist(obj)
  return verb.names.md.pack

## ::
##
##   (setverbcode obj:Obj verb:Str new-code:Str):Obj
##   (setverbcode obj:Obj verb-index:Int new-code:Str):Obj
##
## This builtin is used to set the source code of a verb. The verb should be
## unambiguously referred to by ``verb`` or ``verb-index`` on ``obj`` to avoid
## any issues. The programmer of this task must have write permissions on
## the verb or ``E_PERM`` is rasied.
##
## If the verb fails to parse, ``E_PARSE`` is raised with the appropriate
## message.
##
## If the verb fails to compile, ``E_COMPILE`` is raised with the appropriate
## message.
defBuiltin "setverbcode":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbcode takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let newCode = extractString(args[2])

  let err = verb.setCode(newCode, verb.owner)
  checkForError(err)
  world.persist(obj)
  return nilD.pack

## ::
##
##   (getverbcode obj:Obj verb:Str):Str
##   (getverbcode obj:Obj verb-index:Int):Str
##
## Retrieves the code for the verb ``verb`` on ``obj`` (or the verb pointed to
## by ``verb-index``). If the programmer does not have read permissions on the
## verb, ``E_PERM`` is raised.`
defBuiltin "getverbcode":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbcode takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj

  checkRead(verb)

  return verb.code.md.pack

# EXPERIMENTAL
defBuiltin "getverbbytecode":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbcode takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj

  checkRead(verb)

  var res: seq[MData] = @[]
  for i in verb.compiled.code:
    res.add( @[($i.itype)[2..^1].mds, i.operand, i.pos.line.md, i.pos.col.md].md )

  return res.md.pack

# (move what dest)
defBuiltin "move":
  if args.len < 2: # We are actually allowed more arguments in later phases
    runtimeError(E_ARGS, "move takes 2 arguments")

  let
    whatd = args[0]
    destd = args[1]

  var
    what = extractObject(whatd)
    dest = extractObject(destd)

  var phase = phase # So we can change it

  let oldLoc = what.getLocation()

  if phase == 0: # Check for acceptance
    if args.len != 2:
      runtimeError(E_ARGS, "move takes 2 arguments")

    checkOwn(what)

    let failure = isNil(dest.verbCall("accept", player, caller, @[what.md], callback = task.id))
    if failure: # We were not able to call the verb
      runtimeError(E_FMOVE, "$1 didn't accept $2" % [dest.toObjStr(), what.toObjStr()])

    task.setStatus(tsAwaitingResult)
    return 1.pack

  if phase == 1: # Check for recursive move and call exitfunc
    let accepted = args[2]
    if not accepted.truthy:
      runtimeError(E_FMOVE, "$1 didn't accept $2" % [dest.toObjStr(), what.toObjStr()])

    var conductor = dest

    while not isNil(conductor):
      if conductor == what:
        runtimeError(E_RECMOVE, "moving $1 to $2 is recursive" % [what.toObjStr(), dest.toObjStr()])
      let loc = conductor.getLocation()
      if loc == conductor:
        break
      conductor = loc

    if isNil(oldLoc):
      phase += 1
    else:
      let failure = isNil(oldLoc.verbCall("exitfunc", player, caller, @[what.md], callback = task.id))
      if failure:
        # This means the verb didn't exist, but that's not an issue.
        phase += 1
      else:
        task.setStatus(tsAwaitingResult)
        return 2.pack

  if phase == 2:
    var moveSucceeded = what.moveTo(dest)

    world.persist(what)
    world.persist(dest)
    if not isNil(oldLoc):
      world.persist(oldLoc)

    if not moveSucceeded:
      runtimeError(E_FMOVE, "moving $1 to $2 failed (it could already be at $2)" %
            [what.toObjStr(), dest.toObjStr()])

    let failure = isNil(dest.verbCall("enterfunc", player, caller, @[what.md], callback = task.id))
    if failure:
      phase += 1
    else:
      task.setStatus(tsAwaitingResult)
      return 3.pack

  if phase == 3:
    return what.md.pack

# (create parent new-owner)
# creates a child of the object given
# owner is set to executor of code
#   (in the case of verbs, the owner of the verb)
#
defBuiltin "create":
  let alen = args.len
  if alen != 1 and alen != 2:
    runtimeError(E_ARGS, "create takes 1 or 2 arguments")

  let parent = extractObject(args[0])

  var newOwner: MObject
  if alen == 2:
    newOwner = extractObject(args[1])

    if newOwner != owner and not isWizardT():
      runtimeError(E_PERM, "non-wizards can only set themselves as the owner of objects")
  else:
    newOwner = owner

  if not parent.fertile and (owns(parent) or isWizardT()):
    runtimeError(E_PERM, "$1 is not fertile" % [parent.toObjStr()])

  # TODO: Quotas

  let newObj = parent.createChild()
  world.add(newObj)
  newObj.setPropR("name", "child of $1 ($2)" % [$parent.md, $newObj.md])

  discard newObj.verbCall("initialize", player, caller, @[])

  newObj.owner = newOwner
  # TODO: some way to keep track of an object's owner objects

  world.persist(newObj)
  world.persist(parent)

  return newObj.md.pack

# (playerflag obj [new-val])
# If new-val is not specified, this builtin returns obj's
# player bit (1 if it's a player, 0 if not.) If it is specified,
# obj's player's bit is set to it if task's owner is a wizard
defBuiltin "playerflag":
  case args.len:
    of 1:
      let obj = extractObject(args[0])
      return obj.isPlayer.int.md.pack
    of 2:
      let obj = extractObject(args[0])
      if not isWizardT():
        runtimeError(E_PERM, "only wizards can set the player flag")
      let newVal = extractInt(args[1])
      if newVal != 0 and newVal != 1:
        runtimeError(E_ARGS, "player flag can only be set to 0 or 1")
      obj.isPlayer = newVal == 1
      return obj.md.pack
    else:
      runtimeError(E_ARGS, "playerflag takes 1 or 2 arguments")

## ::
##
##   (level obj:Obj):Int
##
## Returns ``obj``'s permission level.
##
## ::
##
##    0   wizard
##    1   programmer
##    2   builder
##    3   regular
defBuiltin "level":
  if args.len != 1:
    runtimeError(E_ARGS, "level takes 1 argument")

  let obj = extractObject(args[0])
  return obj.level.md.pack

## ::
##
##   (setlevel obj:Obj new-level:Int):Int
##
## Sets ``obj``'s level to ``new-level``. If the level is not within ``0..3``
## then ``E_ARGS`` is raised. If the programmer isn't a wizard then ``E_PERM``
## is raised.
##
## Examples::
##
##   (setlevel #59 3) ; => 3
##   (setlevel #59 5) ; => E_ARGS
##   (setlevel #59 3) ; => E_PERM (if the programmer wasn't a wizard)
defBuiltin "setlevel":
  if args.len != 2:
    runtimeError(E_ARGS, "setlevel takes 2 arguments")

  let obj = extractObject(args[0])
  let newLevel = extractInt(args[1])

  if not isWizardT():
    runtimeError(E_PERM, "only wizards can set level")

  if newLevel > 3 or newLevel < 0:
    runtimeError(E_ARGS, "level must be 0..3")

  obj.level = newLevel
  return newLevel.md.pack

# (recycle obj)
#
# Destroys obj
defBuiltin "recycle":
  var phase = phase

  let nowhered = world.getGlobal("nowhere")
  let nowhere = extractObject(nowhered)

  if phase == 0:
    if args.len != 1:
      runtimeError(E_ARGS, "recycle takes 1 argument")

    let obj = extractObject(args[0])
    checkOwn(obj)

    let contents = obj.getContents()
    for contained in contents:
      # This is actually how LambdaMOO does it: no call to the move
      # builtin instead a raw exitfunc call. I find this somewhat
      # dubious but actually making a builtin call is difficult because
      # of the phase mechanism.
      discard contained.moveTo(nowhere)
      discard obj.verbCall("exitfunc", player, caller, @[contained.md])
      world.persist(contained)

    if isNil(obj.verbCall("recycle", player, caller, @[], callback = task.id)):
      # We don't actually care if the verb "recycle" exists
      phase = 1
    else:
      task.setStatus(tsAwaitingResult)
      return 1.pack

  if phase == 1:
    let obj = extractObject(args[0])
    let parent = obj.parent

    # If the object parents itself, all of its children will be left
    # without a parent so they now have to parent themselves too.
    if parent == obj:
      while obj.children.len > 0:
        let child = obj.children[0]
        child.changeParent(child)
        world.persist(child)
    else:
      while obj.children.len > 0:
        let child = obj.children[0]
        child.changeParent(parent)
        world.persist(child)

    let childIndex = parent.children.find(obj)
    if childIndex > -1:
      system.delete(parent.children, childIndex)

    # Destroy the object
    discard obj.moveTo(nowhere)
    discard nowhere.removeFromContents(obj)
    world.dbDelete(obj)
    world.persist()

    return 1.md.pack

defBuiltin "maxobj":
  if args.len != 0:
    runtimeError(E_ARGS, "maxobj takes no arguments")

  return (world.getObjects()[].len - 1).md.pack

## ::
##
##   (renumber obj:Obj):Obj
##
## Assigns a new object number to ``obj``. Updates all mentions of ``obj`` in
## the parent/child hierarchy and location/contents hierarchy but nowhere else.
## The new number will be the lowest available object number that isn't equal
## to ``obj``'s number.
##
## This should **not** be used unless you know what you're doing.
##
## For the following examples, Imagine ``#10`` has a child ``#12`` and an
## object ``#11`` in its contents. Naturally, ``#11`` has ``location`` set to
## ``#10`` and ``#12``'s parent is ``#10``.
##
## Examples::
##
##   (renumber #10)  ; => #13  (#0 - #9 are already taken)
##   (getprop #10 "contents") ; => E_ARGS  (#10 doesn't exist anymore!)
##   (getprop #13 "contents") ; => (#12)
##   (getprop #12 "location") ; => (#13)
##   (parent #11) ; => #13
##
defBuiltin "renumber":
  runtimeError(E_BUILTIN, "renumber not implemented yet!")

  # if args.len != 1:
  #   runtimeError(E_ARGS, "renumber takes 1 argument")

  # if not isWizardT():
  #   runtimeError(E_PERM, "only wizards can renumber objects")

  # let obj = extractObject(args[0])
  # let worldObjects = world.getObjects
  # var newNumber = worldObjects[].len

  # for idx, wobj in worldObjects[]:
  #   if isNil(wobj):
  #     newNumber = idx

  # let currentNumber = obj.getID().int
  # world.dbDelete(obj)

  # obj.id = ObjID(newNumber)


## ::
##
##   (parent obj:Obj):Obj
##
## Returns the parent object of ``obj``.
##
## Examples::
##
##   (parent #5) ; => #1
##
defBuiltin "parent":
  if args.len != 1:
    runtimeError(E_ARGS, "parent takes 1 argument")

  let obj = extractObject(args[0])
  return obj.parent.md.pack

## ::
##
##   (children obj:Obj):List
##
## Returns a list of ``obj``'s children. Only immediate children are returned,
## not children of these children.
##
## Examples::
##
##   (children #1) ; => (#2 #4 #8 #10 #45 #90)  (arbitrary example)
defBuiltin "children":
  if args.len != 1:
    runtimeError(E_ARGS, "children takes 1 argument")

  let obj = extractObject(args[0])
  return obj.children.map(md).md.pack

defBuiltin "setparent":
  if args.len != 2:
    runtimeError(E_ARGS, "setparent takes 2 arguments")

  var obj = extractObject(args[0])
  let oldParent = obj.parent

  let newParent = extractObject(args[1])
  if not newParent.fertile and not isWizardT():
    runtimeError(E_PERM, "cannot create child of infertile parent")

  var conductor = newParent

  # An object can parent itself but cannot parent a different object that
  # parents it. This is because having #0 parent of #0 and #1 parent of #1
  # is useful. This is an important decision that can go awry later though.
  #
  # Note that the original LambdaMOO doesn't allow for recursive parenting
  # at all.
  while conductor != conductor.parent:
    conductor = conductor.parent
    if conductor == obj:
      runtimeError(E_RECMOVE, "parenting cannot create cycles of length greater than 1!")

  obj.changeParent(newParent)
  world.persist(oldParent)
  world.persist(obj)
  world.persist(newParent)

  return newParent.md.pack

# (query player str)
# Queries for `str` from `player`'s perspective
# It just wraps the query proc in querying.nim
defBuiltin "query":
  if args.len != 2:
    runtimeError(E_ARGS, "query takes 2 arguments")

  let who = extractObject(args[0])
  let str = extractString(args[1])

  if not (isWizardT() or owns(who) or who == player):
    runtimeError(E_PERM, "cannot query from perspective of " & player.toObjStr())

  return who.query(str).map(md).md.pack

## ::
##
##   (istype x:Any typedesc:Str):Int
##
## Checks if ``x`` is of the type represented by the string ``typedesc``.
## Possible values of ``typedesc`` are::
##
##   int        integer
##   float      floating point number
##   str        string
##   sym        symbol
##   obj        object
##   list       list of anything
##   err        error
##   nil        the value 'nil'
##
## If an invalid type is given, then ``E_ARGS`` is raised.  If ``x`` is indeed
## of the type described by ``typedesc`` then ``1`` is returned. If not, ``0``
## is.
##
## Examples::
##
##   (istype "hello world" "str") ; => 1
##   (istype 5 "int")             ; => 1
##   (istype #100 "str")          ; => 0
##   (istype nil "nil")           ; => 1
defBuiltin "istype":
  if args.len != 2:
    runtimeError(E_ARGS, "istype takes 2 arguments")

  let
    what = args[0]
    typev = extractString(args[1])

  let (valid, typedVal) = strToType(typev)
  if not valid:
    runtimeError(E_ARGS, "'$1' is not a valid data type" % typev)

  if what.isType(typedVal):
    return 1.md.pack
  else:
    return 0.md.pack

## ::
##
##   (valid obj:Obj):Int
##
## Check if ``obj`` is valid, meaning it actually refers to an object in the
## database. If so, ``1`` is returned. If not, ``0`` is.
##
## Examples::
##
##   (valid #0) ; => 1
##   (valid #49293) ; => 0  (assuming there isn't a #49293, of course)
defBuiltin "valid":
  if args.len != 1:
    runtimeError(E_ARGS, "valid takes one argument")

  let objd = args[0]
  checkType(objd, dObj)
  let objO = world.dataToObj(objd)
  if objO.isSome():
    return 1.md.pack
  else:
    return 0.md.pack


# (call lambda-or-builtin args)
# forces evaluation (is this a good way to do it?)
defBuiltin "call":
  if args.len < 1:
    runtimeError(E_ARGS, "call takes one or more argument (lambda then arguments)")

  let execd = args[0]
  if execd.isType(dSym):
    let stmt = (@[execd] & args[1 .. ^1]).md

    return stmt.pack
  elif execd.isType(dList):
    var lambl = execd.listVal
    if lambl.len != 3:
      runtimeError(E_ARGS, "call: invalid lambda")

    lambl = lambl & args[1 .. ^1]

    return lambl.md.pack
  else:
    runtimeError(E_ARGS, "call's first argument must be a builtin symbol or a lambda")

# (verbcall obj verb-desc (arg0 arg1 arg2 ...))
defBuiltin "verbcall":
  var args = args
  case args.len
    of 2:
      args.add(@[].md)
    of 3:
      discard
    of 4:
      discard
    else:
      runtimeError(E_ARGS, "verbcall takes 2 or 3 arguments")

  let obj = extractObject(args[0])
  let (holder, verb) = getVerbOn(args[0], args[1], all = true)

  if phase == 0:
    if args.len > 3:
      runtimeError(E_ARGS, "verbcall takes 2 or 3 arguments")

    let cargs = extractList(args[2])

    checkExecute(verb)

    let verbTask = obj.verbCallRaw(
      verb = verb,
      player = player,
      caller = self,
      cargs, symtable = symtable,
      holder = holder,
      taskType = task.taskType, callback = task.id
    )

    if isNil(verbTask):
      runtimeError(E_VERBNF, "verb $#:$# has not been compiled (perhaps it failed earlier?)" %
                                [obj.toObjStr(), verb.names])
    task.setStatus(tsAwaitingResult)
    return 1.pack

  if phase == 1:
    let verbResult = args[^1]

    return verbResult.pack
    # TODO: increment current task's quotas by the amount of ticks innerTask used

type
  BinFloatOp = proc(x: float, y: float): float
  ShortCircuitType = enum scNone, scOr, scAnd

template extractFloatInto(into: var float, num: MData) =
  if num.isType(dInt):
    into = num.intVal.float
  elif num.isType(dFloat):
    into = num.floatVal
  else:
    runtimeError(E_ARGS, "invalid number " & $num)

template defArithmeticOperator(name: string, op: BinFloatOp, logical = false,
                               strictlyBinary = false, shortCircuit = scNone) =
  defBuiltin name:
    if strictlyBinary:
      if args.len != 2:
        runtimeError(E_ARGS, "$1 takes 2 arguments" % name)
    else:
      if args.len < 2:
        runtimeError(E_ARGS, "$1 takes 2 or more arguments" % name)

    var rhs, lhs: float
    template combine(lhsd: MData, rhsd: MData): MData =

      lhs.extractFloatInto(lhsd)
      rhs.extractFloatInto(rhsd)

      if lhsd.isType(dInt) and rhsd.isType(dInt):
        op(lhs, rhs).int.md
      else:
        op(lhs, rhs).md

    var acc: MData

    if logical:
      acc = args[0].truthy.int.md
      for next in args[1 .. ^1]:
        if shortCircuit == scOr and acc.truthy:
          break
        if shortCircuit == scAnd and not acc.truthy:
          break

        acc = combine(acc, next.truthy.int.md)
    else:
      acc = args[0]
      for next in args[1 .. ^1]:
        acc = combine(acc, next)

    return acc.pack

defArithmeticOperator("+", `+`)
defArithmeticOperator("-", `-`)
defArithmeticOperator("*", `*`)
defArithmeticOperator("/", `/`)

proc wrappedAnd(a, b: float): float = (a.int and b.int).float
proc wrappedOr(a, b: float): float = (a.int or b.int).float
proc wrappedXor(a, b: float): float = (a.int xor b.int).float

defArithmeticOperator("&", wrappedAnd)
defArithmeticOperator("|", wrappedOr)
defArithmeticOperator("^", wrappedXor)
defArithmeticOperator("and", wrappedAnd, logical = true, shortCircuit = scAnd)
defArithmeticOperator("or",  wrappedOr, logical = true, shortCircuit = scOr)
defArithmeticOperator("xor", wrappedXor, logical = true)

proc wrappedLT(a, b: float): float = (a < b).float
proc wrappedLTE(a, b: float): float = (a <= b).float
proc wrappedGT(a, b: float): float = (a > b).float<commit_msg>[builtin] Add mod operator<commit_after># Here are all of the builtin functions that verbs can call

import strutils
import tables
import sequtils
import math
import nre
import options
import times

import types
import objects
import verbs
import scripting
import persist
import compile
import tasks
import querying
import server

# for hashing builtins
import bcrypt

# Provided by bcrypt, but not exported; used for the `random` builtin
# TODO: find an alternative?
proc arc4random: int32 {.importc: "arc4random".}

proc strToType(str: string): tuple[b: bool, t: MDataType] =
  case str.toLowerAscii():
    of "int": return (true, dInt)
    of "float": return (true, dFloat)
    of "str": return (true, dStr)
    of "sym": return (true, dSym)
    of "obj": return (true, dObj)
    of "list": return (true, dList)
    of "table": return (true, dTable)
    of "err": return (true, dErr)
    of "nil": return (true, dNil)
    else: return (false, dInt)

proc typeToStr(typ: MDataType): string =
  case typ:
    of dInt:   "int"
    of dFloat: "float"
    of dStr:   "str"
    of dSym:   "sym"
    of dObj:   "obj"
    of dList:  "list"
    of dTable: "table"
    of dErr:   "err"
    of dNil:   "nil"

# turns all %s to \s because %s are easier to use within the server
proc escapeRegex(pat: string): Regex =
  re(pat.replace(re"%(.)", proc (m: RegexMatch): string =
    let capt = m.captures[0]
    if capt == "%":
      return "%"
    else:
      return "\\" & capt))

# Convenience templates: these are to be called from builtins to extract
# values from their arguments but raising an error if they're not of the
# correct data type.
template extractInt(d: MData): int =
  let dV = d
  checkType(dV, dInt)
  dV.intVal

template extractFloat(d: MData): float =
  let dV = d
  var res: float
  if dV.isType(dFloat):
    res = dV.floatVal
  elif dV.isType(dInt):
    res = dV.intVal.float
  else:
    let msg = "$#: expected argument of type int or float, instead got $#"
    runtimeError(E_TYPE, msg % [bname, typeToStr(dV.dtype)])

  res

template extractString(d: MData): string =
  let dV = d
  checkType(dV, dStr)
  dV.strVal
template extractList(d: MData): seq[MData] =
  let dV = d
  checkType(dV, dList)
  dV.listVal
template extractTable(d: MData): Table[MData, MData] =
  let dV = d
  checkType(dV, dTable)
  dV.tableVal
template extractError(d: MData): tuple[e: MError, s: string] =
  let dV = d
  checkType(dV, dErr)
  (dV.errVal, dV.errMsg)

template extractObject(objd: MData): MObject =
  let objdV = objd
  checkType(objdV, dObj)
  let obj = world.dataToObj(objdV)
  if not obj.isSome():
    runtimeError(E_ARGS, "invalid object " & $objdV)

  obj.get()

# Error-checking templates:
# Builtins need to check permissions and types. If any of these
# checks fail, then a error is thrown in the program.
template checkForError(value: MData) =
  let valueV = value
  if valueV.isType(dErr) and valueV.errVal != E_NONE:
    return valueV.pack

template runtimeError(error: MError, message: string) =
  return error.md(message).pack

template checkType(value: MData, expected: MDataType, ifnot: MError = E_TYPE) =
  let valueV = value
  let expectedV = expected
  if not valueV.isType(expectedV):
    runtimeError(ifnot,
      bname & ": expected argument of type " & typeToStr(expectedV) &
        " instead got " & typeToStr(valueV.dType))

template isWizardT: bool = isWizard(task.owner)
template owns(what: MObject): bool = task.owner.owns(what)

template checkOwn(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.owns(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " doesn't own " & whatV.toObjStr())

template checkRead(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read " & whatV.toObjStr())
template checkWrite(what: MObject) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write " & whatV.toObjStr())
template checkRead(what: MProperty) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read property: " & whatV.name)
template checkWrite(what: MProperty) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write property: " & whatV.name)
template checkRead(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canRead(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot read verb: " & whatV.names)
template checkWrite(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canWrite(whatV):
    runtimeError(E_PERM, obj.toObjStr() & " cannot write verb: " & whatV.names)
template checkExecute(what: MVerb) =
  let whatV = what
  let obj = task.owner
  if not obj.canExecute(verb):
    runtimeError(E_PERM, obj.toObjStr() & " cannot execute verb: " & whatV.names)

# This is so that strings can be echoed without the quotes surrounding them.
proc toEchoString*(x: MData): string =
  if x.isType(dStr):
    x.strVal
  else:
    $x

## ::
##
##   (echo arg1:Any ...):List
##
## Takes any number of arguments of any type and outputs them to object on
## which the verb is being run on. Each argument is passed to the
## `toEchoString` proc before being output.
##
## ::
##
##   (let ((obj #5))
##     (echo "object " obj " is an object"))
##
##   ;;; This outputs "object #5 is an object" to self
##
## The argument list is simply returned.
##
## NOTE: This is probably not what you want to call because you want to echo to
## the player who typed the verb. If this builtin is called from a verb running
## on some object that isn't a player, nobody will see he message. (look into
## the ``notify`` builtin)
defBuiltin "echo":
  var
    newArgs: seq[MData] = @[]
    sendstr = ""
  for arg in args:
    let res = arg
    sendstr &= res.toEchoString()
    newArgs.add(res)
  self.send(sendstr)
  return newArgs.md.pack

## ::
##
##   (notify player:Obj message:Str):Str
##
## Sends a line of output ``message`` to ``player``. There are some
## restrictions, however. The programmer who's permissions this task running
## with must either own ``player`` or be a wizard. If one of these conditions
## isn't met, `E_PERM` is raised.
##
## The value returned is the string that was printed.
defBuiltin "notify":
  if args.len != 2:
    runtimeError(E_ARGS, "notify takes 2 arguments")

  let who = extractObject(args[0])
  let msg = extractString(args[1])

  if task.owner != who and not isWizardT():
    runtimeError(E_PERM, "$# cannot notify $#" % [$owner, $who])

  who.send(msg)
  return msg.md.pack

## ::
##
##   (do expr1:Any expr2:Any ...):Any
##
## This builtin evaluates each expression passed to it in order and returns the
## result of the last one evaluated. This is one way to do multiple things in a
## lambda:
##
## ::
##
##   (lambda (x)
##     (do
##      (mangle x)
##      (eat x)
##      (clean-up)))
defBuiltin "do":
  var newArgs: seq[MData] = @[]
  for arg in args:
    let res = arg
    newArgs.add(res)

  if newArgs.len > 0:
    return newArgs[^1].pack
  else:
    return @[].md.pack

## ::
##
##   (parse code:String):Any
##
## Parses ``code`` and returns the object that it was

defBuiltin "parse":
  if args.len != 1:
    runtimeError(E_ARGS, "parse takes 1 argument")

  let code = extractString(args[0])

  var parser = newParser(code)
  let parsed = parser.parseAtom()
  checkForError(parser.error)

  return parsed.pack

## ::
##
##   (eval form:Any):Any
##
## Treats ``form`` as actual code, evaluates it, and returns the result.
## Internally, the code is compiled and loaded into a whole new task which is
## then run. **Use sparingly**.
defBuiltin "eval":
  if phase == 0:
    if args.len != 1:
      runtimeError(E_ARGS, "eval takes 1 argument")

    let form = args[0]

    # Design choice: the compiler runs with the permissions of THE PLAYER WHO TYPED THE COMMAND.
    # This is used for executing compile-time code.
    let instructions = compileCode(form, player)
    checkForError(instructions.error)

    discard world.addTask("eval", self, player, caller, owner, symtable, instructions,
                        taskType = task.taskType, callback = task.id)
    task.setStatus(tsAwaitingResult)
    return 1.pack
  if phase == 1:
    return args[1].pack

## ::
##
##   (settaskperms new-perms:Obj):Obj
##
## Sets the current task's permissions to those of ``new-perms`` (which is also
## returned). The main use of this is the idiom:
##
## ::
##
##   (setttaskperms caller)
##
## To make sure that a verb is no longer running with wizard permissions and
## doesn't royally screw things up.
defBuiltin "settaskperms":
  if args.len != 1:
    runtimeError(E_ARGS, "settaskperms takes 1 argument")

  let newPerms = extractObject(args[0])
  if not isWizardT() and task.owner != newPerms:
    runtimeError(E_PERM, "$# can't set task perms to $#" %
                  [task.owner.toObjStr(), newPerms.toObjStr()])

  task.owner = newPerms
  return newPerms.md.pack

## ::
##
##   (callerperms):Obj
##
## Returns the object whose permissions the current task is running
## with.
defBuiltin "callerperms":
  if args.len != 0:
    runtimeError(E_ARGS, "callerperms takes no arguments")

  return task.owner.md.pack

## ::
##
##   (read [player:Obj]):Str
##
## Reads a line of input from ``player``'s connection (if omitted it reads
## from the player who entered the command that started this task).
##
## The programmer must own ``player`` or be a wizard or ``E_PERM`` is raised.
##
## **Use extremely sparingly**.
defBuiltin "read":
  if phase == 0:
    var who: MObject
    case args.len:
      of 0:
        who = player
      of 1:
        who = extractObject(args[0])
      else:
        runtimeError(E_ARGS, "read takes 0 or 1 arguments")

    if not isWizardT() and who != caller:
      runtimeError(E_PERM, "you don't have permission to read from that connection")

    let client = findClient(who)
    if isNil(client):
      runtimeError(E_ARGS, who.toObjStr() & " has not been connected to!")

    task.askForInput(client)
    return 1.inputPack
  elif phase == 1:
    # sanity check
    if args.len != 1:
      runtimeError(E_ARGS, "read failed")

    return args[0].pack

## ::
##
##   (err error-type:Err message:Str)
##
## Raises an error with type ``error-type`` and message ``message``.
##
## Example of use::
##
##   (if (> count 10)
##       (err E_ARGS "too many")
##       (do-something-with count))
defBuiltin "err":
  if args.len != 2:
    runtimeError(E_ARGS, "err takes 2 arguments")

  var err = args[0]
  checkType(err, dErr)

  let msg = extractString(args[1])

  err.errMsg = msg
  err.trace = @[]
  return err.pack

## ::
##
##   (erristype err:Err err2:Err):Int
##
## Checks whether ``err`` has same error type as ``err2``. If true, it returns
## ``1``, if not ``0``. This is useful in ``try`` expressions:
##
## ::
##
##   (try (error-prone-code)
##        (if (erristype error E_PERM)     ; note: in catch block, `error` holds the error
##            (echo "Permission error")
##            (echo "Some other kind of error)))
defBuiltin "erristype":
  if args.len != 2:
    runtimeError(E_ARGS, "erristype takes 2 arguments")

  var (err, _) = extractError(args[0])
  var (err2, _) = extractError(args[1])

  return if err == err2: 1.md.pack else: 0.md.pack

proc extractInfo(prop: MProperty): MData =
  var res: seq[MData] = @[]
  res.add(prop.owner.md)

  var perms = ""
  if prop.pubRead: perms &= "r"
  if prop.pubWrite: perms &= "w"
  if prop.ownerIsParent: perms &= "c"

  res.add(perms.md)
  return res.md

proc extractInfo(verb: MVerb): MData =
  var res: seq[MData] = @[]
  res.add(verb.owner.md)

  var perms = ""
  if verb.pubRead: perms &= "r"
  if verb.pubWrite: perms &= "w"
  if verb.pubExec: perms &= "x"

  res.add(perms.md)
  res.add(verb.names.md)
  return res.md

proc extractArgs(verb: MVerb): MData =
  var res: seq[MData] = @[]
  res.add(objSpecToStr(verb.doSpec).md)
  res.add(prepSpecToStr(verb.prepSpec).md)
  res.add(objSpecToStr(verb.ioSpec).md)

  return res.md

type
  PropInfo = tuple[owner: MObject, perms: string, newName: string]
  VerbInfo = tuple[owner: MObject, perms: string, newName: string]
  VerbArgs = tuple[doSpec: ObjSpec, prepSpec: PrepType, ioSpec: ObjSpec]

template propInfoFromInput(info: seq[MData]): PropInfo =
  if info.len != 2 and info.len != 3:
    runtimeError(E_ARGS, "property info must be a list of size 2 or 3")

  var res: PropInfo

  let ownerd = info[0]
  let ownero = extractObject(ownerd)
  res.owner = ownero

  let perms = extractString(info[1])
  res.perms = perms

  if info.len == 3:
    let newName = extractString(info[2])
    res.newName = newName

  res

template verbInfoFromInput(info: seq[MData]): VerbInfo =
  if info.len != 2 and info.len != 3:
    runtimeError(E_ARGS, "verb info must be a list of size 2 or 3")

  var res: VerbInfo

  let ownerd = info[0]
  let ownero = extractObject(ownerd)
  res.owner = ownero

  let perms = extractString(info[1])
  res.perms = perms

  if info.len == 3:
    let newName = extractString(info[2])
    res.newName = newName

  res

template objSpecFromData(ospd: MData): ObjSpec =
  let str = extractString(ospd)
  let ospec = strToObjSpec(str)

  if not ospec.isSome():
    runtimeError(E_ARGS, "invalid object spec '$1'" % str)

  ospec.get()

template prepSpecFromData(pspd: MData): PrepType =
  let str = extractString(pspd)
  let pspec = strToPrepSpec(str)

  if not pspec.isSome():
    runtimeError(E_ARGS, "invalid preposition spec '$1'" % str)

  pspec.get()

template verbArgsFromInput(info: seq[MData]): VerbArgs =
  if info.len != 3:
    runtimeError(E_ARGS, "verb args must be a list of size 3")

  var vargs: VerbArgs
  vargs.doSpec = objSpecFromData(info[0])
  vargs.prepSpec = prepSpecFromData(info[1])
  vargs.ioSpec = objSpecFromData(info[2])

  vargs

proc setInfo(prop: MProperty, info: PropInfo) =
  prop.owner = info.owner
  prop.pubRead = "r" in info.perms
  prop.pubWrite = "w" in info.perms
  prop.ownerIsParent = "c" in info.perms

  prop.name = info.newName

proc setInfo(verb: MVerb, info: VerbInfo) =
  verb.owner = info.owner
  verb.pubRead = "r" in info.perms
  verb.pubWrite = "w" in info.perms
  verb.pubExec = "x" in info.perms

  verb.names = info.newName

proc setArgs(verb: MVerb, args: VerbArgs) =
  verb.doSpec = args.doSpec
  verb.prepSpec = args.prepSpec
  verb.ioSpec = args.ioSpec

template getPropOn(objd, propd: MData, die = true, useDefault = false,
                   default = nilD, all = false, inherited = true):
                     tuple[o: MObject, p: MProperty] =
  let objd2 = objd
  let obj = extractObject(objd2)
  var res: tuple[o: MObject, p: MProperty]

  let
    propName = extractString(propd)
    (objOn, propObj) = obj.getPropAndObj(propName, all)

  if isNil(propObj):
    if useDefault:
      res = (nil, newProperty("default", default, nil))
    else:
      if die:
        runtimeError(E_PROPNF, "property $1 not found on $2" % [propName, $obj.toObjStr()])
      else:
        return nilD.pack
  else:
    if not all and not inherited and obj.propIsInherited(propObj):
      runtimeError(E_PROPNF, "property $1 not found on $2" % [propName, $obj.toObjStr()])

    res = (objOn, propObj)
  res

template getVerbOn(objd, verbdescd: MData, die = true,
                   all = false): tuple[o: MObject, v: MVerb] =

  let objd2 = objd
  let obj = extractObject(objd2)

  var res: tuple[o: MObject, v: MVerb]

  let verbdescd2 = verbdescd
  if verbdescd2.isType(dStr):
    let verbdesc = verbdescd2.strVal

    let (objOn, verb) = obj.getVerbAndObj(verbdesc, all)
    if isNil(verb):
      if die:
        runtimeError(E_VERBNF, "verb $1 not found on $2" % [verbdesc, obj.toObjStr()])
      else:
        return nilD.pack

    res = (objOn, verb)
  elif verbdescd2.isType(dInt):
    let verbnum = verbdescd2.intVal

    if verbnum >= obj.verbs.len or verbnum < 0:
      runtimeError(E_VERBNF, "verb index $# out of range" % $verbnum)

    res = (obj, obj.verbs[verbnum])
  else:
    runtimeError(E_ARGS, "verb indices can only be strings or integers")

  res

## ::
##
##   (getprop obj:Obj prop:Str):Any
##
## Gets the property `prop` of `obj`. If the property doesn't exist,
## ``E_PROPNF`` is raised. The programmer should also have read access on the
## object. If not, ``E_PERM`` is raised.
##
## There is a shorthand for this builtin but it must be used carefully because
## it is rudimentary:
##
## ::
##
##   (echo "The value of #5.name is " #5.name)
##
## instead of:
##
## ::
##
##   (echo "The value of #5.name is " (getprop #5 "name"))
##
## In short, ``obj.prop`` expands to ``(getpop obj "prop")``, but don't expect
## it to work with verb calls: ``obj.prop:verb()`` **will not** work and neither
## will shenanigans like ``(callerperms).name``. However, these can be nested:
## ``obj.location.name`` will work.
defBuiltin "getprop":
  var useDefault = false
  var default = nilD
  case args.len:
    of 2: discard
    of 3:
      default = args[2]
      useDefault = true
    else:
      runtimeError(E_ARGS, "getprop takes 2 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1],
                                 all = true,
                                 default = default,
                                 useDefault = useDefault)
  discard obj

  checkRead(propObj)

  return propObj.val.pack

## ::
##
##   (setprop obj:Obj prop:Str new-val:Any):Any
##
## Companion of ``getprop``. It sets the property described by ``prop`` on
## ``obj`` to ``new-val``. If ``prop`` cannot be found on ``obj`` then
## ``E_PROPNF`` is raised. If the programmer doesn't have write access on
## ``obj`` then ``E_PERM`` is raised. The value returned is the value that was
## set.
##
## There is no cute shorthand for this builtin to avoid overcomplicating the
## syntax.
defBuiltin "setprop":
  if args.len != 3:
    runtimeError(E_ARGS, "setprop takes 3 arguments")

  let objd = args[0]
  let obj = extractObject(objd)

  let newVal = args[2]

  let propName = extractString(args[1])
  var oldProp = obj.getProp(propName, all = false)

  if isNil(oldProp):
    checkWrite(obj)
    let (newProp, error) = obj.setProp(propName, newVal)
    checkForError(error)

    newProp.owner = task.owner
    world.persist(obj)
  else:
    checkWrite(oldProp)
    # Even though we could just do oldProp.val = newVal,
    # we need to call setProp. This is because there are
    # some special properties whose types are enforced.
    let (_, error) = obj.setProp(propName, newVal)
    checkForError(error)

    if propName == "owner":
      # The following line is safe because setProp would have returned an error
      # if the property "owner" was set to a non-object
      let newOwner = newVal.extractObject()
      for prop in obj.props:
        if prop.ownerIsParent:
          prop.owner = newOwner

    world.persist(obj)

  return newVal.pack

## ::
##
##   (delprop obj:Obj prop:Str):Obj
##
## This builtin is for deleting a property from an object and all its
## descendants. If the property is not found, then ``E_PROPNF`` is raised. If
## the programmer does not have write permissions on ``obj`` then ``E_PERM`` is
## raised.  The return value is ``obj``.
defBuiltin "delprop":
  if args.len != 2:
    runtimeError(E_ARGS, "delprop takes 2 arguments")

  let (obj, prop) = getPropOn(args[0], args[1], inherited = false)

  for moddedObj, deletedProp in obj.delPropRec(prop).items:
    discard deletedProp
    world.persist(moddedObj)

  return obj.md.pack

## ::
##
##   (getpropinfo obj:Obj prop:Str):List
##
## Retrieves information about the property referred to by ``prop`` on ``obj``.
## This information is a list whose first element is the owner of the property.
## The second element is a string of characters taken from the set: ``r``, ``w`,
## and ``c``. ``r`` signifies that the property is publicly readable. ``w``
## signifies that the property is publicly writable. ``c`` signifies that the
## owner of this property in this object's descendants should be the owner of
## the child, not the owner of this object.
##
## If the property doesn't exist, then ``E_PROPNF`` is raised. If the
## programmer does not have read access to ``obj`` then ``E_PERM`` is raised.
defBuiltin "getpropinfo":
  if args.len != 2:
    runtimeError(E_ARGS, "getpropinfo takes 2 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1])
  discard obj

  checkRead(propObj)

  return extractInfo(propObj).pack


## ::
##
##   (setpropinfo obj:Obj prop:Str new-info:List):Obj
##
## Sets the property ``prop`` on ``obj``'s info to ``new-info``. ``new-info``
## is specified by ``(new-owner:Obj new-perms:Str [new-name:Str])``.
## ``new-perms`` is in the same format as the part of the result of
## ``getpropinfo``.
##
## If the property does not exist, ``E_PROPNF`` is raised and if the programmer
## does not have write access to the ``obj`` then ``E_PERM`` is raised.
defBuiltin "setpropinfo":
  if args.len != 3:
    runtimeError(E_ARGS, "setpropinfo takes 3 arguments")

  let (obj, propObj) = getPropOn(args[0], args[1])

  checkWrite(propObj)

  # validate the property info
  let
    propinfo = extractList(args[2])
    info = propInfoFromInput(propinfo)

  propObj.setInfo(info)
  world.persist(obj)


  return args[0].pack

## ::
##
##   (props obj:Obj):List
##
## Returns a list of properties defined directly on ``obj``. If the programmer
## does not have read access to ``obj``, ``E_PERM`` is raised.
defBuiltin "props":
  if args.len != 1:
    runtimeError(E_ARGS, "props takes 1 argument")

  let objd = args[0]
  let obj = extractObject(objd)

  checkRead(obj)

  let res = obj.getOwnProps().map(md)

  return res.md.pack

## ::
##
##   (verbs obj:Obj):List
##
## Returns a list of verbs defined directly on ``obj``. If the programmer does
## not have read access to ``obj``, ``E_PERM`` is raised.
defBuiltin "verbs":
  if args.len != 1:
    runtimeError(E_ARGS, "verbs takes 1 argument")

  let objd = args[0]
  let obj = extractObject(objd)

  checkRead(obj)

  var res: seq[MData] = @[]
  for v in obj.verbs:
    res.add(v.names.md)

  return res.md.pack


## ::
##
##   (getverbinfo obj:Obj verb:Str):List
##   (getverbinfo obj:Obj verb-index:Int):List
##
## See the documentation of ``getpropinfo`` for the format of the list
## returned.  The only difference is that instead of ``c`` in the permissions
## there is ``x`` which signifies if the verb is publicly executable.
##
## If ``verb`` is not found on ``obj``, then ``E_VERBNF`` is raised. If the
## programmer does not have read permissions on the object and the verb then
## ``E_PERM`` is raised.
defBuiltin "getverbinfo":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbinfo takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj
  checkRead(verb)

  return extractInfo(verb).pack

## ::
##
##   (setverbinfo obj:Obj verb:Str new-info:List):Obj
##   (setverbinfo obj:Obj verb-index:Int new-info:List):Obj
##
## This is the companion verb to ``getverbinfo``. The ``new-info`` list
## is of the same format as the return value of ``getverbinfo`` but can
## contain one extra element at the end that specifies a new list of names
## for the verb in a string.
##
## The return value is the ``obj``.
defBuiltin "setverbinfo":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbinfo takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let verbInfo = extractList(args[2])

  let info = verbInfoFromInput(verbInfo)

  verb.setInfo(info)
  world.persist(obj)
  return args[0].pack

## ::
##
##   (getverbargs obj:Obj verb:Str):List
##   (getverbargs obj:Obj verb-index:Int):List
##
## This builtin returns the arguments that the verb is supposed to operate on.
## It will be in the list of the form:
##
## ::
##
##   (direct-object preposition indirect-object)
##
## If ``verb`` does not exist on ``obj`` then ``E_VERBNF`` is raised. If the
## programmer does not have read permissions on ``verb`` then ``E_PERM`` is
## raised.
defBuiltin "getverbargs":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbargs takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj
  checkRead(verb)

  return extractArgs(verb).pack

## ::
##
##   (setverbargs obj:Obj verb:Str new-args:List):Obj
##   (setverbargs obj:Obj verb-index:Int new-args:List):Obj
##
## This is the companion verb to ``getverbargs``. ``setverbinfo`` is to
## ``getverbinfo`` as this verb is to ``getverbargs``. The signature above
## should be enough to infer how to use it. Write access is required or
## ``E_PERM`` is raised.
defBuiltin "setverbargs":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbargs takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let verbArgs = extractList(args[2])

  let argsInfo = verbArgsFromInput(verbArgs)
  verb.setArgs(argsInfo)
  world.persist(obj)

  return args[0].pack

## ::
##
##   (addverb obj:Obj new-verb-names:Str):Obj
##
## Adds a verb to ``obj`` with names specified by ``new-verb-names``. The new
## verb's owner will be the programmer of this verb. If the programmer does not
## have write permissions on ``obj``, ``E_PERM`` is raised. The return value of
## this verb is the object that gained a new verb.
defBuiltin "addverb":
  if args.len != 2:
    runtimeError(E_ARGS, "addverb takes 2 arguments")

  let objd = args[0]
  let obj = extractObject(objd)

  checkWrite(obj)

  let names = extractString(args[1])

  var verb = newVerb(
    names = names,
    owner = owner,
  )

  checkForError(verb.setCode("", owner))

  discard obj.addVerb(verb)
  world.persist(obj)

  return objd.pack

## ::
##
##   (delverb obj:Obj verb:Str):Obj
##   (delverb obj:Obj verb-index:Int):Obj
##
## Deletes the verb with name ``verb`` or index ``verb-index`` off ``obj`` and
## all of its descendants. If the string ``verb`` does not point to any verb on
## ``obj`` then ``E_VERBNF`` is raised. If the programmer doesn't have write
## access to the ``obj`` then ``E_PERM`` is raised.
##
## The return value is the ``obj``.
defBuiltin "delverb":
  if args.len != 2:
    runtimeError(E_ARGS, "delverb takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])

  checkWrite(obj)

  if isNil(verb) or verb.inherited:
    runtimeError(E_VERBNF, "$1 does not define a verb $2" % [obj.toObjStr, $args[1]])

  discard obj.delVerb(verb)
  world.persist(obj)

  return obj.md.pack


defBuiltin "loadverb":
  if args.len != 2:
    runtimeError(E_ARGS, "loadverb takes 2 arguments (object and verb name)")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let resultd = readVerbCode(world, obj, verb, player)
  checkForError(resultd)
  world.persist(obj)
  return verb.names.md.pack

## ::
##
##   (setverbcode obj:Obj verb:Str new-code:Str):Obj
##   (setverbcode obj:Obj verb-index:Int new-code:Str):Obj
##
## This builtin is used to set the source code of a verb. The verb should be
## unambiguously referred to by ``verb`` or ``verb-index`` on ``obj`` to avoid
## any issues. The programmer of this task must have write permissions on
## the verb or ``E_PERM`` is rasied.
##
## If the verb fails to parse, ``E_PARSE`` is raised with the appropriate
## message.
##
## If the verb fails to compile, ``E_COMPILE`` is raised with the appropriate
## message.
defBuiltin "setverbcode":
  if args.len != 3:
    runtimeError(E_ARGS, "setverbcode takes 3 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  checkWrite(verb)

  let newCode = extractString(args[2])

  let err = verb.setCode(newCode, verb.owner)
  checkForError(err)
  world.persist(obj)
  return nilD.pack

## ::
##
##   (getverbcode obj:Obj verb:Str):Str
##   (getverbcode obj:Obj verb-index:Int):Str
##
## Retrieves the code for the verb ``verb`` on ``obj`` (or the verb pointed to
## by ``verb-index``). If the programmer does not have read permissions on the
## verb, ``E_PERM`` is raised.`
defBuiltin "getverbcode":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbcode takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj

  checkRead(verb)

  return verb.code.md.pack

# EXPERIMENTAL
defBuiltin "getverbbytecode":
  if args.len != 2:
    runtimeError(E_ARGS, "getverbcode takes 2 arguments")

  let (obj, verb) = getVerbOn(args[0], args[1])
  discard obj

  checkRead(verb)

  var res: seq[MData] = @[]
  for i in verb.compiled.code:
    res.add( @[($i.itype)[2..^1].mds, i.operand, i.pos.line.md, i.pos.col.md].md )

  return res.md.pack

# (move what dest)
defBuiltin "move":
  if args.len < 2: # We are actually allowed more arguments in later phases
    runtimeError(E_ARGS, "move takes 2 arguments")

  let
    whatd = args[0]
    destd = args[1]

  var
    what = extractObject(whatd)
    dest = extractObject(destd)

  var phase = phase # So we can change it

  let oldLoc = what.getLocation()

  if phase == 0: # Check for acceptance
    if args.len != 2:
      runtimeError(E_ARGS, "move takes 2 arguments")

    checkOwn(what)

    let failure = isNil(dest.verbCall("accept", player, caller, @[what.md], callback = task.id))
    if failure: # We were not able to call the verb
      runtimeError(E_FMOVE, "$1 didn't accept $2" % [dest.toObjStr(), what.toObjStr()])

    task.setStatus(tsAwaitingResult)
    return 1.pack

  if phase == 1: # Check for recursive move and call exitfunc
    let accepted = args[2]
    if not accepted.truthy:
      runtimeError(E_FMOVE, "$1 didn't accept $2" % [dest.toObjStr(), what.toObjStr()])

    var conductor = dest

    while not isNil(conductor):
      if conductor == what:
        runtimeError(E_RECMOVE, "moving $1 to $2 is recursive" % [what.toObjStr(), dest.toObjStr()])
      let loc = conductor.getLocation()
      if loc == conductor:
        break
      conductor = loc

    if isNil(oldLoc):
      phase += 1
    else:
      let failure = isNil(oldLoc.verbCall("exitfunc", player, caller, @[what.md], callback = task.id))
      if failure:
        # This means the verb didn't exist, but that's not an issue.
        phase += 1
      else:
        task.setStatus(tsAwaitingResult)
        return 2.pack

  if phase == 2:
    var moveSucceeded = what.moveTo(dest)

    world.persist(what)
    world.persist(dest)
    if not isNil(oldLoc):
      world.persist(oldLoc)

    if not moveSucceeded:
      runtimeError(E_FMOVE, "moving $1 to $2 failed (it could already be at $2)" %
            [what.toObjStr(), dest.toObjStr()])

    let failure = isNil(dest.verbCall("enterfunc", player, caller, @[what.md], callback = task.id))
    if failure:
      phase += 1
    else:
      task.setStatus(tsAwaitingResult)
      return 3.pack

  if phase == 3:
    return what.md.pack

# (create parent new-owner)
# creates a child of the object given
# owner is set to executor of code
#   (in the case of verbs, the owner of the verb)
#
defBuiltin "create":
  let alen = args.len
  if alen != 1 and alen != 2:
    runtimeError(E_ARGS, "create takes 1 or 2 arguments")

  let parent = extractObject(args[0])

  var newOwner: MObject
  if alen == 2:
    newOwner = extractObject(args[1])

    if newOwner != owner and not isWizardT():
      runtimeError(E_PERM, "non-wizards can only set themselves as the owner of objects")
  else:
    newOwner = owner

  if not parent.fertile and (owns(parent) or isWizardT()):
    runtimeError(E_PERM, "$1 is not fertile" % [parent.toObjStr()])

  # TODO: Quotas

  let newObj = parent.createChild()
  world.add(newObj)
  newObj.setPropR("name", "child of $1 ($2)" % [$parent.md, $newObj.md])

  discard newObj.verbCall("initialize", player, caller, @[])

  newObj.owner = newOwner
  # TODO: some way to keep track of an object's owner objects

  world.persist(newObj)
  world.persist(parent)

  return newObj.md.pack

# (playerflag obj [new-val])
# If new-val is not specified, this builtin returns obj's
# player bit (1 if it's a player, 0 if not.) If it is specified,
# obj's player's bit is set to it if task's owner is a wizard
defBuiltin "playerflag":
  case args.len:
    of 1:
      let obj = extractObject(args[0])
      return obj.isPlayer.int.md.pack
    of 2:
      let obj = extractObject(args[0])
      if not isWizardT():
        runtimeError(E_PERM, "only wizards can set the player flag")
      let newVal = extractInt(args[1])
      if newVal != 0 and newVal != 1:
        runtimeError(E_ARGS, "player flag can only be set to 0 or 1")
      obj.isPlayer = newVal == 1
      return obj.md.pack
    else:
      runtimeError(E_ARGS, "playerflag takes 1 or 2 arguments")

## ::
##
##   (level obj:Obj):Int
##
## Returns ``obj``'s permission level.
##
## ::
##
##    0   wizard
##    1   programmer
##    2   builder
##    3   regular
defBuiltin "level":
  if args.len != 1:
    runtimeError(E_ARGS, "level takes 1 argument")

  let obj = extractObject(args[0])
  return obj.level.md.pack

## ::
##
##   (setlevel obj:Obj new-level:Int):Int
##
## Sets ``obj``'s level to ``new-level``. If the level is not within ``0..3``
## then ``E_ARGS`` is raised. If the programmer isn't a wizard then ``E_PERM``
## is raised.
##
## Examples::
##
##   (setlevel #59 3) ; => 3
##   (setlevel #59 5) ; => E_ARGS
##   (setlevel #59 3) ; => E_PERM (if the programmer wasn't a wizard)
defBuiltin "setlevel":
  if args.len != 2:
    runtimeError(E_ARGS, "setlevel takes 2 arguments")

  let obj = extractObject(args[0])
  let newLevel = extractInt(args[1])

  if not isWizardT():
    runtimeError(E_PERM, "only wizards can set level")

  if newLevel > 3 or newLevel < 0:
    runtimeError(E_ARGS, "level must be 0..3")

  obj.level = newLevel
  return newLevel.md.pack

# (recycle obj)
#
# Destroys obj
defBuiltin "recycle":
  var phase = phase

  let nowhered = world.getGlobal("nowhere")
  let nowhere = extractObject(nowhered)

  if phase == 0:
    if args.len != 1:
      runtimeError(E_ARGS, "recycle takes 1 argument")

    let obj = extractObject(args[0])
    checkOwn(obj)

    let contents = obj.getContents()
    for contained in contents:
      # This is actually how LambdaMOO does it: no call to the move
      # builtin instead a raw exitfunc call. I find this somewhat
      # dubious but actually making a builtin call is difficult because
      # of the phase mechanism.
      discard contained.moveTo(nowhere)
      discard obj.verbCall("exitfunc", player, caller, @[contained.md])
      world.persist(contained)

    if isNil(obj.verbCall("recycle", player, caller, @[], callback = task.id)):
      # We don't actually care if the verb "recycle" exists
      phase = 1
    else:
      task.setStatus(tsAwaitingResult)
      return 1.pack

  if phase == 1:
    let obj = extractObject(args[0])
    let parent = obj.parent

    # If the object parents itself, all of its children will be left
    # without a parent so they now have to parent themselves too.
    if parent == obj:
      while obj.children.len > 0:
        let child = obj.children[0]
        child.changeParent(child)
        world.persist(child)
    else:
      while obj.children.len > 0:
        let child = obj.children[0]
        child.changeParent(parent)
        world.persist(child)

    let childIndex = parent.children.find(obj)
    if childIndex > -1:
      system.delete(parent.children, childIndex)

    # Destroy the object
    discard obj.moveTo(nowhere)
    discard nowhere.removeFromContents(obj)
    world.dbDelete(obj)
    world.persist()

    return 1.md.pack

defBuiltin "maxobj":
  if args.len != 0:
    runtimeError(E_ARGS, "maxobj takes no arguments")

  return (world.getObjects()[].len - 1).md.pack

## ::
##
##   (renumber obj:Obj):Obj
##
## Assigns a new object number to ``obj``. Updates all mentions of ``obj`` in
## the parent/child hierarchy and location/contents hierarchy but nowhere else.
## The new number will be the lowest available object number that isn't equal
## to ``obj``'s number.
##
## This should **not** be used unless you know what you're doing.
##
## For the following examples, Imagine ``#10`` has a child ``#12`` and an
## object ``#11`` in its contents. Naturally, ``#11`` has ``location`` set to
## ``#10`` and ``#12``'s parent is ``#10``.
##
## Examples::
##
##   (renumber #10)  ; => #13  (#0 - #9 are already taken)
##   (getprop #10 "contents") ; => E_ARGS  (#10 doesn't exist anymore!)
##   (getprop #13 "contents") ; => (#12)
##   (getprop #12 "location") ; => (#13)
##   (parent #11) ; => #13
##
defBuiltin "renumber":
  runtimeError(E_BUILTIN, "renumber not implemented yet!")

  # if args.len != 1:
  #   runtimeError(E_ARGS, "renumber takes 1 argument")

  # if not isWizardT():
  #   runtimeError(E_PERM, "only wizards can renumber objects")

  # let obj = extractObject(args[0])
  # let worldObjects = world.getObjects
  # var newNumber = worldObjects[].len

  # for idx, wobj in worldObjects[]:
  #   if isNil(wobj):
  #     newNumber = idx

  # let currentNumber = obj.getID().int
  # world.dbDelete(obj)

  # obj.id = ObjID(newNumber)


## ::
##
##   (parent obj:Obj):Obj
##
## Returns the parent object of ``obj``.
##
## Examples::
##
##   (parent #5) ; => #1
##
defBuiltin "parent":
  if args.len != 1:
    runtimeError(E_ARGS, "parent takes 1 argument")

  let obj = extractObject(args[0])
  return obj.parent.md.pack

## ::
##
##   (children obj:Obj):List
##
## Returns a list of ``obj``'s children. Only immediate children are returned,
## not children of these children.
##
## Examples::
##
##   (children #1) ; => (#2 #4 #8 #10 #45 #90)  (arbitrary example)
defBuiltin "children":
  if args.len != 1:
    runtimeError(E_ARGS, "children takes 1 argument")

  let obj = extractObject(args[0])
  return obj.children.map(md).md.pack

defBuiltin "setparent":
  if args.len != 2:
    runtimeError(E_ARGS, "setparent takes 2 arguments")

  var obj = extractObject(args[0])
  let oldParent = obj.parent

  let newParent = extractObject(args[1])
  if not newParent.fertile and not isWizardT():
    runtimeError(E_PERM, "cannot create child of infertile parent")

  var conductor = newParent

  # An object can parent itself but cannot parent a different object that
  # parents it. This is because having #0 parent of #0 and #1 parent of #1
  # is useful. This is an important decision that can go awry later though.
  #
  # Note that the original LambdaMOO doesn't allow for recursive parenting
  # at all.
  while conductor != conductor.parent:
    conductor = conductor.parent
    if conductor == obj:
      runtimeError(E_RECMOVE, "parenting cannot create cycles of length greater than 1!")

  obj.changeParent(newParent)
  world.persist(oldParent)
  world.persist(obj)
  world.persist(newParent)

  return newParent.md.pack

# (query player str)
# Queries for `str` from `player`'s perspective
# It just wraps the query proc in querying.nim
defBuiltin "query":
  if args.len != 2:
    runtimeError(E_ARGS, "query takes 2 arguments")

  let who = extractObject(args[0])
  let str = extractString(args[1])

  if not (isWizardT() or owns(who) or who == player):
    runtimeError(E_PERM, "cannot query from perspective of " & player.toObjStr())

  return who.query(str).map(md).md.pack

## ::
##
##   (istype x:Any typedesc:Str):Int
##
## Checks if ``x`` is of the type represented by the string ``typedesc``.
## Possible values of ``typedesc`` are::
##
##   int        integer
##   float      floating point number
##   str        string
##   sym        symbol
##   obj        object
##   list       list of anything
##   err        error
##   nil        the value 'nil'
##
## If an invalid type is given, then ``E_ARGS`` is raised.  If ``x`` is indeed
## of the type described by ``typedesc`` then ``1`` is returned. If not, ``0``
## is.
##
## Examples::
##
##   (istype "hello world" "str") ; => 1
##   (istype 5 "int")             ; => 1
##   (istype #100 "str")          ; => 0
##   (istype nil "nil")           ; => 1
defBuiltin "istype":
  if args.len != 2:
    runtimeError(E_ARGS, "istype takes 2 arguments")

  let
    what = args[0]
    typev = extractString(args[1])

  let (valid, typedVal) = strToType(typev)
  if not valid:
    runtimeError(E_ARGS, "'$1' is not a valid data type" % typev)

  if what.isType(typedVal):
    return 1.md.pack
  else:
    return 0.md.pack

## ::
##
##   (valid obj:Obj):Int
##
## Check if ``obj`` is valid, meaning it actually refers to an object in the
## database. If so, ``1`` is returned. If not, ``0`` is.
##
## Examples::
##
##   (valid #0) ; => 1
##   (valid #49293) ; => 0  (assuming there isn't a #49293, of course)
defBuiltin "valid":
  if args.len != 1:
    runtimeError(E_ARGS, "valid takes one argument")

  let objd = args[0]
  checkType(objd, dObj)
  let objO = world.dataToObj(objd)
  if objO.isSome():
    return 1.md.pack
  else:
    return 0.md.pack


# (call lambda-or-builtin args)
# forces evaluation (is this a good way to do it?)
defBuiltin "call":
  if args.len < 1:
    runtimeError(E_ARGS, "call takes one or more argument (lambda then arguments)")

  let execd = args[0]
  if execd.isType(dSym):
    let stmt = (@[execd] & args[1 .. ^1]).md

    return stmt.pack
  elif execd.isType(dList):
    var lambl = execd.listVal
    if lambl.len != 3:
      runtimeError(E_ARGS, "call: invalid lambda")

    lambl = lambl & args[1 .. ^1]

    return lambl.md.pack
  else:
    runtimeError(E_ARGS, "call's first argument must be a builtin symbol or a lambda")

# (verbcall obj verb-desc (arg0 arg1 arg2 ...))
defBuiltin "verbcall":
  var args = args
  case args.len
    of 2:
      args.add(@[].md)
    of 3:
      discard
    of 4:
      discard
    else:
      runtimeError(E_ARGS, "verbcall takes 2 or 3 arguments")

  let obj = extractObject(args[0])
  let (holder, verb) = getVerbOn(args[0], args[1], all = true)

  if phase == 0:
    if args.len > 3:
      runtimeError(E_ARGS, "verbcall takes 2 or 3 arguments")

    let cargs = extractList(args[2])

    checkExecute(verb)

    let verbTask = obj.verbCallRaw(
      verb = verb,
      player = player,
      caller = self,
      cargs, symtable = symtable,
      holder = holder,
      taskType = task.taskType, callback = task.id
    )

    if isNil(verbTask):
      runtimeError(E_VERBNF, "verb $#:$# has not been compiled (perhaps it failed earlier?)" %
                                [obj.toObjStr(), verb.names])
    task.setStatus(tsAwaitingResult)
    return 1.pack

  if phase == 1:
    let verbResult = args[^1]

    return verbResult.pack
    # TODO: increment current task's quotas by the amount of ticks innerTask used

type
  BinFloatOp = proc(x: float, y: float): float
  ShortCircuitType = enum scNone, scOr, scAnd

template extractFloatInto(into: var float, num: MData) =
  if num.isType(dInt):
    into = num.intVal.float
  elif num.isType(dFloat):
    into = num.floatVal
  else:
    runtimeError(E_ARGS, "invalid number " & $num)

template defArithmeticOperator(name: string, op: BinFloatOp, logical = false,
                               strictlyBinary = false, shortCircuit = scNone) =
  defBuiltin name:
    if strictlyBinary:
      if args.len != 2:
        runtimeError(E_ARGS, "$1 takes 2 arguments" % name)
    else:
      if args.len < 2:
        runtimeError(E_ARGS, "$1 takes 2 or more arguments" % name)

    var rhs, lhs: float
    template combine(lhsd: MData, rhsd: MData): MData =

      lhs.extractFloatInto(lhsd)
      rhs.extractFloatInto(rhsd)

      if lhsd.isType(dInt) and rhsd.isType(dInt):
        op(lhs, rhs).int.md
      else:
        op(lhs, rhs).md

    var acc: MData

    if logical:
      acc = args[0].truthy.int.md
      for next in args[1 .. ^1]:
        if shortCircuit == scOr and acc.truthy:
          break
        if shortCircuit == scAnd and not acc.truthy:
          break

        acc = combine(acc, next.truthy.int.md)
    else:
      acc = args[0]
      for next in args[1 .. ^1]:
        acc = combine(acc, next)

    return acc.pack

defArithmeticOperator("+", `+`)
defArithmeticOperator("-", `-`)
defArithmeticOperator("*", `*`)
defArithmeticOperator("/", `/`)
defArithmeticOperator("%", `mod`)

proc wrappedAnd(a, b: float): float = (a.int and b.int).float
proc wrappedOr(a, b: float): float = (a.int or b.int).float
proc wrappedXor(a, b: float): float = (a.int xor b.int).float

defArithmeticOperator("&", wrappedAnd)
defArithmeticOperator("|", wrappedOr)
defArithmeticOperator("^", wrappedXor)
defArithmeticOperator("and", wrappedAnd, logical = true, shortCircuit = scAnd)
defArithmeticOperator("or",  wrappedOr, logical = true, shortCircuit = scOr)
defArithmeticOperator("xor", wrappedXor, logical = true)

proc wrappedLT(a, b: float): float = (a < b).float
proc wrappedLTE(a, b: float): float = (a <= b).float
proc wrappedGT(a, b: float): float = (a > b).float
{'fraction_non_alphanumeric': 0.13327673075419214, 'fraction_numerical': 0.009361768681644006, 'mean_word_length': 3.3533702947184127, 'pattern_counts': {'":': 110, '<': 17, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2092536', 'n_tokens_mistral': 31323, 'n_tokens_neox': 30125, 'n_words': 9822}