summary refs log tree commit diff stats
path: root/compiler/semobjconstr.nim
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/semobjconstr.nim')
-rw-r--r--compiler/semobjconstr.nim292
1 files changed, 292 insertions, 0 deletions
diff --git a/compiler/semobjconstr.nim b/compiler/semobjconstr.nim
new file mode 100644
index 000000000..f4c225526
--- /dev/null
+++ b/compiler/semobjconstr.nim
@@ -0,0 +1,292 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2015 Nim Contributors
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## This module implements Nim's object construction rules.
+
+# included from sem.nim
+
+type
+  InitStatus = enum
+    initUnknown
+    initFull     # All  of the fields have been initialized
+    initPartial  # Some of the fields have been initialized
+    initNone     # None of the fields have been initialized
+    initConflict # Fields from different branches have been initialized
+
+proc mergeInitStatus(existing: var InitStatus, newStatus: InitStatus) =
+  case newStatus
+  of initConflict:
+    existing = newStatus
+  of initPartial:
+    if existing in {initUnknown, initFull, initNone}:
+      existing = initPartial
+  of initNone:
+    if existing == initUnknown:
+      existing = initNone
+    elif existing == initFull:
+      existing = initPartial
+  of initFull:
+    if existing == initUnknown:
+      existing = initFull
+    elif existing == initNone:
+      existing = initPartial
+  of initUnknown:
+    discard
+
+proc locateFieldInInitExpr(field: PSym, initExpr: PNode): PNode =
+  # Returns the assignment nkExprColonExpr node or nil
+  let fieldId = field.name.id
+  for i in 1 .. <initExpr.len:
+    let assignment = initExpr[i]
+    internalAssert assignment.kind == nkExprColonExpr
+
+    if fieldId == considerQuotedIdent(assignment[0]).id:
+      return assignment
+
+proc semConstrField(c: PContext, flags: TExprFlags,
+                    field: PSym, initExpr: PNode): PNode =
+  let assignment = locateFieldInInitExpr(field, initExpr)
+  if assignment != nil:
+    if nfSem in assignment.flags: return assignment[1]
+    if not fieldVisible(c, field):
+      localError(initExpr.info,
+        "the field '$1' is not accessible.", [field.name.s])
+      return
+
+    var initValue = semExprFlagDispatched(c, assignment[1], flags)
+    if initValue != nil:
+      initValue = fitNode(c, field.typ, initValue, assignment.info)
+    assignment.sons[0] = newSymNode(field)
+    assignment.sons[1] = initValue
+    assignment.flags.incl nfSem
+    return initValue
+
+proc caseBranchMatchesExpr(branch, matched: PNode): bool =
+  for i in 0 .. (branch.len - 2):
+    if exprStructuralEquivalent(branch[i], matched):
+      return true
+
+  return false
+
+proc pickCaseBranch(caseExpr, matched: PNode): PNode =
+  # XXX: Perhaps this proc already exists somewhere
+  let endsWithElse = caseExpr{-1}.kind == nkElse
+  for i in 1 .. caseExpr.len - 1 - int(endsWithElse):
+    if caseExpr[i].caseBranchMatchesExpr(matched):
+      return caseExpr[i]
+
+  if endsWithElse:
+    return caseExpr{-1}
+
+iterator directFieldsInRecList(recList: PNode): PNode =
+  # XXX: We can remove this case by making all nkOfBranch nodes
+  # regular. Currently, they try to avoid using nkRecList if they
+  # include only a single field
+  if recList.kind == nkSym:
+    yield recList
+  else:
+    internalAssert recList.kind == nkRecList
+    for field in recList:
+      if field.kind != nkSym: continue
+      yield field
+
+template quoteStr(s: string): string = "'" & s & "'"
+
+proc fieldsPresentInInitExpr(fieldsRecList, initExpr: PNode): string =
+  result = ""
+  for field in directFieldsInRecList(fieldsRecList):
+    let assignment = locateFieldInInitExpr(field.sym, initExpr)
+    if assignment != nil:
+      if result.len != 0: result.add ", "
+      result.add field.sym.name.s.quoteStr
+
+proc missingMandatoryFields(fieldsRecList, initExpr: PNode): string =
+  for r in directFieldsInRecList(fieldsRecList):
+    if {tfNotNil, tfNeedsInit} * r.sym.typ.flags != {}:
+      let assignment = locateFieldInInitExpr(r.sym, initExpr)
+      if assignment == nil:
+        if result == nil:
+          result = r.sym.name.s
+        else:
+          result.add ", "
+          result.add r.sym.name.s
+
+proc checkForMissingFields(recList, initExpr: PNode) =
+  let missing = missingMandatoryFields(recList, initExpr)
+  if missing != nil:
+    localError(initExpr.info, "fields not initialized: $1.", [missing])
+
+proc semConstructFields(c: PContext, recNode: PNode,
+                        initExpr: PNode, flags: TExprFlags): InitStatus =
+  result = initUnknown
+
+  case recNode.kind
+  of nkRecList:
+    for field in recNode:
+      let status = semConstructFields(c, field, initExpr, flags)
+      mergeInitStatus(result, status)
+
+  of nkRecCase:
+    template fieldsPresentInBranch(branchIdx: int): string =
+      fieldsPresentInInitExpr(recNode[branchIdx]{-1}, initExpr)
+
+    template checkMissingFields(branchNode: PNode) =
+      checkForMissingFields(branchNode{-1}, initExpr)
+
+    let discriminator = recNode.sons[0];
+    internalAssert discriminator.kind == nkSym
+    var selectedBranch = -1
+
+    for i in 1 .. <recNode.len:
+      let innerRecords = recNode[i]{-1}
+      let status = semConstructFields(c, innerRecords, initExpr, flags)
+      if status notin {initNone, initUnknown}:
+        mergeInitStatus(result, status)
+        if selectedBranch != -1:
+          let prevFields = fieldsPresentInBranch(selectedBranch)
+          let currentFields = fieldsPresentInBranch(i)
+          localError(initExpr.info,
+            "The fields ($1) and ($2) cannot be initialized together, " &
+            "because they are from conflicting branches in the case object.",
+            [prevFields, currentFields])
+          result = initConflict
+        else:
+          selectedBranch = i
+
+    if selectedBranch != -1:
+      let branchNode = recNode[selectedBranch]
+      let flags = flags*{efAllowDestructor} + {efNeedStatic, efPreferNilResult}
+      let discriminatorVal = semConstrField(c, flags,
+                                            discriminator.sym, initExpr)
+      if discriminatorVal == nil:
+        let fields = fieldsPresentInBranch(selectedBranch)
+        localError(initExpr.info,
+          "you must provide a compile-time value for the discriminator '$1' " &
+          "in order to prove that it's safe to initialize $2.",
+          [discriminator.sym.name.s, fields])
+        mergeInitStatus(result, initNone)
+      else:
+        let discriminatorVal = discriminatorVal.skipHidden
+
+        template wrongBranchError(i) =
+          let fields = fieldsPresentInBranch(i)
+          localError(initExpr.info,
+            "a case selecting discriminator '$1' with value '$2' " &
+            "appears in the object construction, but the field(s) $3 " &
+            "are in conflict with this value.",
+            [discriminator.sym.name.s, discriminatorVal.renderTree, fields])
+
+        if branchNode.kind != nkElse:
+          if not branchNode.caseBranchMatchesExpr(discriminatorVal):
+            wrongBranchError(selectedBranch)
+        else:
+          # With an else clause, check that all other branches don't match:
+          for i in 1 .. (recNode.len - 2):
+            if recNode[i].caseBranchMatchesExpr(discriminatorVal):
+              wrongBranchError(i)
+              break
+
+      # When a branch is selected with a partial match, some of the fields
+      # that were not initialized may be mandatory. We must check for this:
+      if result == initPartial:
+        checkMissingFields branchNode
+
+    else:
+      result = initNone
+      let discriminatorVal = semConstrField(c, flags + {efPreferStatic},
+                                            discriminator.sym, initExpr)
+      if discriminatorVal == nil:
+        # None of the branches were explicitly selected by the user and no
+        # value was given to the discrimator. We can assume that it will be
+        # initialized to zero and this will select a particular branch as
+        # a result:
+        let matchedBranch = recNode.pickCaseBranch newIntLit(0)
+        checkMissingFields matchedBranch
+      else:
+        result = initPartial
+        if discriminatorVal.kind == nkIntLit:
+          # When the discriminator is a compile-time value, we also know
+          # which brach will be selected:
+          let matchedBranch = recNode.pickCaseBranch discriminatorVal
+          if matchedBranch != nil: checkMissingFields matchedBranch
+        else:
+          # All bets are off. If any of the branches has a mandatory
+          # fields we must produce an error:
+          for i in 1 .. <recNode.len: checkMissingFields recNode[i]
+
+  of nkSym:
+    let field = recNode.sym
+    let e = semConstrField(c, flags, field, initExpr)
+    result = if e != nil: initFull else: initNone
+
+  else:
+    internalAssert false
+
+proc semConstructType(c: PContext, initExpr: PNode,
+                      t: PType, flags: TExprFlags): InitStatus =
+  var t = t
+  result = initUnknown
+
+  while true:
+    let status = semConstructFields(c, t.n, initExpr, flags)
+    mergeInitStatus(result, status)
+
+    if status in {initPartial, initNone, initUnknown}:
+      checkForMissingFields t.n, initExpr
+
+    let base = t.sons[0]
+    if base == nil: break
+    t = skipTypes(base, skipPtrs)
+
+proc semObjConstr(c: PContext, n: PNode, flags: TExprFlags): PNode =
+  var t = semTypeNode(c, n.sons[0], nil)
+  result = newNodeIT(nkObjConstr, n.info, t)
+  for child in n: result.add child
+  
+  t = skipTypes(t, {tyGenericInst, tyAlias})
+  if t.kind == tyRef: t = skipTypes(t.sons[0], {tyGenericInst, tyAlias})
+  if t.kind != tyObject:
+    localError(n.info, errGenerated, "object constructor needs an object type")
+    return
+
+  # Check if the object is fully initialized by recursively testing each
+  # field (if this is a case object, initialized fields in two different
+  # branches will be reported as an error):
+  let initResult = semConstructType(c, result, t, flags)
+
+  # It's possible that the object was not fully initialized while
+  # specifying a .requiresInit. pragma.
+  # XXX: Turn this into an error in the next release
+  if tfNeedsInit in t.flags and initResult != initFull:
+    # XXX: Disable this warning for now, because tfNeedsInit is propagated
+    # too aggressively from fields to object types (and this is not correct
+    # in case objects)
+    when false: message(n.info, warnUser,
+      "object type uses the 'requiresInit' pragma, but not all fields " &
+      "have been initialized. future versions of Nim will treat this as " &
+      "an error")
+
+  # Since we were traversing the object fields, it's possible that
+  # not all of the fields specified in the constructor was visited.
+  # We'll check for such fields here:
+  for i in 1.. <result.len:
+    let field = result[i]
+    if nfSem notin field.flags:
+      let id = considerQuotedIdent(field[0])
+      # This node was not processed. There are two possible reasons:
+      # 1) It was shadowed by a field with the same name on the left
+      for j in 1 .. <i:
+        let prevId = considerQuotedIdent(result[j][0])
+        if prevId.id == id.id:
+          localError(field.info, errFieldInitTwice, id.s)
+          return
+      # 2) No such field exists in the constructed type
+      localError(field.info, errUndeclaredFieldX, id.s)
+      return
+