                 return self
             }
         case .prefixedLocalName:
-            let begin = String.pnCharsU.union(CharacterSet(charactersIn: "0123456789:"))
-            let rest = String.pnChars.union(CharacterSet(charactersIn: ".:"))
-            let last = String.pnChars.union(CharacterSet(charactersIn: ":"))
-
-            return self // TODO
-        }
-    }
-
+            var v = ""
+            /**
+             
+             prefixed name first character:
+             - leave untouched characters in pnCharsU, ':', or [0-9]
+             - backslash escape these characters: ( '_' | '~' | '.' | '-' | '!' | '$' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '/' | '?' | '#' | '@' | '%' )
+             - percent encode anything else
+             **/
+            
+            let okFirst = String.pnCharsU.union(CharacterSet(charactersIn: "0123456789:"))
+            guard let first = self.first else { return "" }
+            let fcs = CharacterSet(first.unicodeScalars)
+            if fcs.isStrictSubset(of: okFirst) {
+                v += String(first)
+            } else if let escaped = String(first).sparqlBackslashEscape {
+                v += escaped
+            } else {
+                v += String(first).sparqlPercentEncoded
+            }
+            
+            /**
+             prefixed name local part (non-first character):
+             - cannot end in a '.'
+             - backslash escape these characters: ( '_' | '~' | '.' | '-' | '!' | '$' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '/' | '?' | '#' | '@' | '%' )
+             - leave untouched characters in pnChars
+             - percent encode anything else
+             
+             **/
+            for c in self.dropFirst(1) {
+                let cs = CharacterSet(c.unicodeScalars)
+                if let escaped = String(c).sparqlBackslashEscape {
+                    v += String(escaped)
+                } else if cs.isStrictSubset(of: String.pnChars) {
+                    v += String(c)
+                } else {
+                    v += String(c).sparqlPercentEncoded
+                }
+            }
+            
+            guard let last = v.last else { return v }
+            if last == "." {
+                v = v.substring(to: v.index(before: v.endIndex))
+                v += ".".sparqlPercentEncoded
+            }
+            
+            return v
+        }
+    }
+
+    private var sparqlPercentEncoded : String {
+        var v = ""
+        for u in self.utf8 {
+            v += String(format: "%%%02d", u)
+        }
+        return v
+    }
+    
+    private var sparqlBackslashEscape : String? {
+        let needsEscaping = CharacterSet(charactersIn: "_~.-!$('()*+,;=/?#@%")
+        var v = ""
+        for c in self {
+            let cs = CharacterSet(c.unicodeScalars)
+            if cs.isStrictSubset(of: needsEscaping) {
+                v += "\\"
+                v += String(c)
+            } else {
+                v += String(c)
+            }
+        }
+        return v
+    }
+    
     private static let pnChars: CharacterSet = {
         var pn = pnCharsU
         pn.insert(charactersIn: "0123456789-")

            }

            let us = UnicodeScalar("\(c)")!
            if SPARQLLexer.pnCharSet.contains(us) {
                if let t = try getPName() {
                    return packageToken(t)
                }

                }
            }
            
            // TODO: verify that we're not projecting a variable more than once; projection is on a Set of variables, though we preserve the ordering for API convenience
            // TODO: add projection for aggregate variables
            algebra = .project(algebra, projection)
        }
