JSON parsing performance (was Re: [PATCH v2] emacs: bad regexp @ `notmuch-search...
authorAustin Clements <amdragon@MIT.EDU>
Wed, 20 Jul 2011 20:50:07 +0000 (16:50 +2000)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:39:11 +0000 (09:39 -0800)
a7/e0a8a2fe52d63827b64ae3b9acdfc5cdd2c2b5 [new file with mode: 0644]

diff --git a/a7/e0a8a2fe52d63827b64ae3b9acdfc5cdd2c2b5 b/a7/e0a8a2fe52d63827b64ae3b9acdfc5cdd2c2b5
new file mode 100644 (file)
index 0000000..488edf6
--- /dev/null
@@ -0,0 +1,1038 @@
+Return-Path: <amdragon@mit.edu>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id EE5BF431FD0\r
+       for <notmuch@notmuchmail.org>; Wed, 20 Jul 2011 13:50:24 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -0.7\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5\r
+       tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id 27QeVyQwmBVx for <notmuch@notmuchmail.org>;\r
+       Wed, 20 Jul 2011 13:50:22 -0700 (PDT)\r
+Received: from dmz-mailsec-scanner-5.mit.edu (DMZ-MAILSEC-SCANNER-5.MIT.EDU\r
+       [18.7.68.34])\r
+       by olra.theworths.org (Postfix) with ESMTP id 8D394431FB6\r
+       for <notmuch@notmuchmail.org>; Wed, 20 Jul 2011 13:50:22 -0700 (PDT)\r
+X-AuditID: 12074422-b7ba7ae000000a14-a6-4e273f7e061b\r
+Received: from mailhub-auth-4.mit.edu ( [18.7.62.39])\r
+       by dmz-mailsec-scanner-5.mit.edu (Symantec Messaging Gateway) with SMTP\r
+       id 1D.A7.02580.E7F372E4; Wed, 20 Jul 2011 16:50:06 -0400 (EDT)\r
+Received: from outgoing.mit.edu (OUTGOING-AUTH.MIT.EDU [18.7.22.103])\r
+       by mailhub-auth-4.mit.edu (8.13.8/8.9.2) with ESMTP id p6KKoLmr011903; \r
+       Wed, 20 Jul 2011 16:50:21 -0400\r
+Received: from awakening.csail.mit.edu (awakening.csail.mit.edu [18.26.4.91])\r
+       (authenticated bits=0)\r
+       (User authenticated as amdragon@ATHENA.MIT.EDU)\r
+       by outgoing.mit.edu (8.13.6/8.12.4) with ESMTP id p6KKoJHH001367\r
+       (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NOT);\r
+       Wed, 20 Jul 2011 16:50:20 -0400 (EDT)\r
+Received: from amthrax by awakening.csail.mit.edu with local (Exim 4.72)\r
+       (envelope-from <amdragon@mit.edu>)\r
+       id 1QjdiZ-0007Ee-Tb; Wed, 20 Jul 2011 16:50:07 -0400\r
+Date: Wed, 20 Jul 2011 16:50:07 -0400\r
+From: Austin Clements <amdragon@MIT.EDU>\r
+To: Pieter Praet <pieter@praet.org>\r
+Subject: JSON parsing performance (was Re: [PATCH v2] emacs: bad regexp @\r
+       `notmuch-search-process-filter')\r
+Message-ID: <20110720205007.GB21316@mit.edu>\r
+References: <20110705214234.GA15360@mit.edu>\r
+       <1310416993-31031-1-git-send-email-pieter@praet.org>\r
+       <20110711210532.GC25558@mit.edu> <878vs28dvo.fsf@praet.org>\r
+       <20110713185721.GI25558@mit.edu>\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed; boundary="/04w6evG8XlLl3ft"\r
+Content-Disposition: inline\r
+In-Reply-To: <20110713185721.GI25558@mit.edu>\r
+User-Agent: Mutt/1.5.20 (2009-06-14)\r
+X-Brightmail-Tracker:\r
+ H4sIAAAAAAAAA+NgFvrIKsWRmVeSWpSXmKPExsUixG6nrltnr+5ncHmrqsW+O1uYLK7fnMls\r
+       8fv1DWaLpb92szmweOx6/pfJY+esu+wez1bdYvbo2HeZNYAlissmJTUnsyy1SN8ugStj5u6n\r
+       7AWr/zBW3G84zdbAuG4PYxcjJ4eEgInExHvv2SFsMYkL99azdTFycQgJ7GOUWLjyMyuEs4FR\r
+       4uOOT8wQzkkmiaU3DkFlljBKfF67C6yfRUBV4vfJNiYQm01AQ2Lb/uVgO0QElCVOP/kJVMPB\r
+       wSxQKLHkWiVIWFggR6Lh4VdmEJtXQEdiz6t1YLaQwDFGiX1LYiDighInZz5hAbGZBawkfv6Z\r
+       wAgxRlpi+T8OEJNTQFdi6aR4kApRARWJa/vb2SYwCs1C0jwLSfMshGaIsJbEjX8vmXAKg9gW\r
+       Ej9+P2XDFDeW2HZ4LuMCRs5VjLIpuVW6uYmZOcWpybrFyYl5ealFuqZ6uZkleqkppZsYwfHo\r
+       orSD8edBpUOMAhyMSjy8DnzqfkKsiWXFlbmHGCU5mJREeafaAYX4kvJTKjMSizPii0pzUosP\r
+       MUpwMCuJ8Cr+U/MT4k1JrKxKLcqHSUlzsCiJ85Z4//cVEkhPLEnNTk0tSC2CycpwcChJ8IoD\r
+       046QYFFqempFWmZOCUKaiYMTZDgP0HBtkBre4oLE3OLMdIj8KUZjjvu37x9h5Lj+FEgKseTl\r
+       56VKifO+ALlRAKQ0ozQPbhospb5iFAd6Tpj3DUgVDzAdw817BbSKCWhVi7oqyKqSRISUVANj\r
+       4l4bltdqfb++PTWNKGasSf1lweD64LSoZtcaGZ+0moNHrX7bfWbmm+n0N3B5c2ClCLPYH/W3\r
+       J5SClDctN1juzrCkj7Exk9lnf4dKlskdZ/nHbdH/Hkx+4rclyG/RLSVb3y1pO6ojMxWsrV36\r
+       5mrU8pW/0O7YdK6Gb+fToylbzZ4bxbILzlBiKc5INNRiLipOBACOvzSrhAMAAA==\r
+Cc: servilio <servilio@gmail.com>, Notmuch Mail <notmuch@notmuchmail.org>,\r
+       David Edmondson <dme@dme.org>\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Wed, 20 Jul 2011 20:50:25 -0000\r
+\r
+\r
+--/04w6evG8XlLl3ft\r
+Content-Type: text/plain; charset=us-ascii\r
+Content-Disposition: inline\r
+\r
+Quoth myself on Jul 13 at  2:57 pm:\r
+> Quoth Pieter Praet on Jul 13 at  4:16 pm:\r
+> > Jamie Zawinski once said/wrote [1]:\r
+> >   'Some people, when confronted with a problem, think "I know,\r
+> >   I'll use regular expressions." Now they have two problems.'\r
+> > \r
+> > With this in mind, I set out to get rid of this whole regex mess altogether,\r
+> > by populating the search buffer using Notmuch's JSON output instead of doing\r
+> > brittle text matching tricks.\r
+> > \r
+> > Looking for some documentation, I stumbled upon a long-forgotten gem [2].\r
+> > \r
+> > David's already done pretty much all of the work for us!\r
+> \r
+> Yes, similar thoughts were running through my head as I futzed with\r
+> the formatting for this.  My concern with moving to JSON for search\r
+> buffers is that parsing it is about *30 times slower* than the current\r
+> regexp-based approach (0.6 seconds versus 0.02 seconds for a mere 1413\r
+> result search buffer).  I think JSON makes a lot of sense for show\r
+> buffers because there's generally less data and it has a lot of\r
+> complicated structure.  Search results, on the other hand, have a very\r
+> simple, regular, and constrained structure, so JSON doesn't buy us\r
+> nearly as much.\r
+> \r
+> JSON is hard to parse because, like the text search output, it's\r
+> designed for human consumption (of course, unlike the text search\r
+> output, it's also designed for computer consumption).  There's\r
+> something to be said for the debuggability and generality of this and\r
+> JSON is very good for exchanging small objects, but it's a remarkably\r
+> inefficient way to exchange large amounts of data between two\r
+> programs.\r
+> \r
+> I guess what I'm getting at, though it pains me to say it, is perhaps\r
+> search needs a fast, computer-readable interchange format.  The\r
+> structure of the data is so simple and constrained that this could be\r
+> altogether trivial.\r
+> \r
+> Or maybe I need a faster computer.\r
+\r
+Or maybe I need to un-lame my benchmark.\r
+\r
+TL;DR: We should use JSON for search results, but possibly not the\r
+json.el shipped with Emacs.\r
+\r
+I realized that my text benchmark didn't capture the cost of\r
+extracting the match strings.  re-search-forward records matches as\r
+buffer positions, which don't get realized into strings until you call\r
+match-string.  Hence, match-string is quite expensive.\r
+\r
+Also, Emacs' json.el is slow, so I perked it up.  My modified json.el\r
+is ~3X faster, particularly for string-heavy output like notmuch's.\r
+Though now I'm well into the realm of "eq is faster than =" and "M-x\r
+disassemble", so unless I missed something big, this is as fast as it\r
+gets.\r
+\r
+While I was still thinking about new IPC formats, I realized that the\r
+text format and the Emacs UI are already tightly coupled, so why not\r
+go all the way and use S-expressions for IPC?  I now think JSON is\r
+fast enough to use, but S-expressions still have a certain appeal.\r
+They share most of the benefits of JSON; structure and extensibility\r
+in particular.  Further, while the content of some ad-hoc format could\r
+easily diverge from both the text and JSON formats, S-expressions\r
+could exactly parallel the JSON content (with a little more\r
+abstraction, they could even share the same format code).  For kicks,\r
+I included an S-expression benchmark.  It beats out the text parser by\r
+a factor of two and the optimized JSON parser by a factor of three.\r
+\r
+Here are the results for my 1,413 result search buffer and timeworn\r
+computer\r
+\r
+                 Time   Normalized\r
+--format=text   0.148s     1.00x\r
+--format=json   0.598s     4.04x\r
+custom json.el  0.209s     1.41x\r
+ + string keys  0.195s     1.32x\r
+S-expressions   0.066s     0.45x\r
+\r
+I don't have time right now, but next week I might be able to look\r
+through and update dme's JSON-based search code.\r
+\r
+\r
+The benchmark and modified json.el are attached.\r
+\r
+The benchmark is written so you can open it and eval-buffer, then C-x\r
+C-e the various calls in the comments.  You can either\r
+make-text/make-json, or run notmuch manually, pipe the results into\r
+files "text" and "json", and open them in Emacs.\r
+\r
+Please excuse the modified json.el code; it's gone through zero\r
+cleanup.\r
+\r
+--/04w6evG8XlLl3ft\r
+Content-Type: text/plain; charset=us-ascii\r
+Content-Disposition: attachment; filename="timeparse.el"\r
+\r
+(defmacro time-it (repeat &rest body)\r
+  (declare (indent 1))\r
+  (when (not (numberp repeat))\r
+    (push repeat body)\r
+    (setq repeat 1))\r
+  (let ((start-time (gensym)) (i (gensym)))\r
+    `(let ((,start-time (get-internal-run-time)))\r
+       (dotimes (,i ,repeat)\r
+        ,@body)\r
+       (/ (float-time (time-subtract (get-internal-run-time) ,start-time))\r
+         ,repeat))))\r
+\r
+;; Text\r
+\r
+(defun make-text ()\r
+  (with-current-buffer (get-buffer-create "text")\r
+    (erase-buffer)\r
+    (call-process "notmuch" nil t nil "search" "--format=text" "--" "tag:x/notmuch")))\r
+\r
+(defun time-text ()\r
+  (with-current-buffer "text"\r
+    (time-it 10\r
+      (goto-char (point-min))\r
+      (while (re-search-forward "^\\(thread:[0-9A-Fa-f]*\\) \\([^][]*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" nil t)\r
+       (let* ((thread-id (match-string 1))\r
+              (date (match-string 2))\r
+              (count (match-string 3))\r
+              (authors (match-string 4))\r
+              (subject (match-string 5))\r
+              (tags (match-string 6))\r
+              (tag-list (if tags (save-match-data (split-string tags)))))\r
+         t)))))\r
+\r
+(byte-compile 'time-text)\r
+;; (make-text)\r
+;; (time-text)\r
+\r
+;; JSON\r
+\r
+(defun load-custom-json ()\r
+  (byte-compile-file "json.el")\r
+  (load-file "./json.elc"))\r
+\r
+(defun make-json ()\r
+  (with-current-buffer (get-buffer-create "json")\r
+    (erase-buffer)\r
+    (call-process "notmuch" nil t nil "search" "--format=json" "--" "tag:x/notmuch")))\r
+\r
+(defun time-json (&optional buf)\r
+  (with-current-buffer (or buf "json")\r
+    (let ((json-array-type 'list)\r
+         (json-object-type 'alist)\r
+         (json-key-type 'symbol))\r
+      (time-it 10\r
+       (goto-char (point-min))\r
+       (dolist (ent (json-read))\r
+         ;; (Surprisingly, traversing the structure has no noticeable\r
+         ;; impact to performance)\r
+         (let ((thread-id (assq 'thread ent))\r
+               (date (assq 'timestamp ent))\r
+               (matched (assq 'matched ent))\r
+               (total (assq 'total ent))\r
+               (authors (assq 'authors ent))\r
+               (subject (assq 'subject ent))\r
+               (tag-list (assq 'tags ent)))\r
+           t))))))\r
+\r
+(defun time-json-string-keys (&optional buf)\r
+  (with-current-buffer (or buf "json")\r
+    (let ((json-array-type 'list)\r
+         (json-object-type 'alist)\r
+         (json-key-type 'string))\r
+      (time-it 10\r
+       (goto-char (point-min))\r
+       (dolist (ent (json-read))\r
+         (let ((thread-id (assoc "thread" ent))\r
+               (date (assoc "timestamp" ent))\r
+               (matched (assoc "matched" ent))\r
+               (total (assoc "total" ent))\r
+               (authors (assoc "authors" ent))\r
+               (subject (assoc "subject" ent))\r
+               (tag-list (assoc "tags" ent)))\r
+           t))))))\r
+\r
+(byte-compile 'time-json)\r
+(byte-compile 'time-json-string-keys)\r
+;; (make-json)\r
+;; (time-json)\r
+;; (time-json-string-keys)\r
+;; (load-custom-json)\r
+\r
+;; S-expression\r
+\r
+(defun make-sexp ()\r
+  (with-current-buffer (get-buffer-create "sexp")\r
+    (erase-buffer))\r
+  (print\r
+   (with-current-buffer "json"\r
+     (let ((json-array-type 'list)\r
+          (json-object-type 'alist)\r
+          (json-key-type 'symbol))\r
+       (goto-char (point-min))\r
+       (json-read)))\r
+   (get-buffer "sexp"))\r
+  t)\r
+\r
+(defun time-sexp ()\r
+  (with-current-buffer "sexp"\r
+    (let ((buf (current-buffer)))\r
+      (time-it 10 (goto-char (point-min)) (read buf)))))\r
+\r
+(byte-compile 'time-sexp)\r
+;; (make-sexp)\r
+;; (time-sexp)\r
+\r
+;; Packed JSON\r
+\r
+(defun make-packed-json ()\r
+  (let ((buf (get-buffer-create "packed-json")))\r
+    (with-current-buffer "json"\r
+      (copy-to-buffer buf (point-min) (point-max)))\r
+    (with-current-buffer buf\r
+      (while (re-search-forward "^\\([^\"]*\"[^\"]+\"\\): \\([[\"0-9]\\)" nil t)\r
+       (replace-match "\\1:\\2" nil nil))\r
+      (goto-char (point-min))\r
+      (while (re-search-forward "\\([\"0-9]\\),\n" nil t)\r
+       (replace-match "\\1," nil nil)))))\r
+\r
+(defun time-packed-json ()\r
+  (time-json "packed-json"))\r
+\r
+;; (make-packed-json)\r
+;; (time-packed-json)\r
+\r
+--/04w6evG8XlLl3ft\r
+Content-Type: text/plain; charset=us-ascii\r
+Content-Disposition: attachment; filename="json.el"\r
+\r
+;;; json.el --- JavaScript Object Notation parser / generator\r
+\r
+;; Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Free Software Foundation, Inc.\r
+\r
+;; Author: Edward O'Connor <ted@oconnor.cx>\r
+;; Version: 1.2\r
+;; Keywords: convenience\r
+\r
+;; This file is part of GNU Emacs.\r
+\r
+;; GNU Emacs is free software: you can redistribute it and/or modify\r
+;; it under the terms of the GNU General Public License as published by\r
+;; the Free Software Foundation, either version 3 of the License, or\r
+;; (at your option) any later version.\r
+\r
+;; GNU Emacs is distributed in the hope that it will be useful,\r
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+;; GNU General Public License for more details.\r
+\r
+;; You should have received a copy of the GNU General Public License\r
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.\r
+\r
+;;; Commentary:\r
+\r
+;; This is a library for parsing and generating JSON (JavaScript Object\r
+;; Notation).\r
+\r
+;; Learn all about JSON here: <URL:http://json.org/>.\r
+\r
+;; The user-serviceable entry points for the parser are the functions\r
+;; `json-read' and `json-read-from-string'. The encoder has a single\r
+;; entry point, `json-encode'.\r
+\r
+;; Since there are several natural representations of key-value pair\r
+;; mappings in elisp (alist, plist, hash-table), `json-read' allows you\r
+;; to specify which you'd prefer (see `json-object-type' and\r
+;; `json-array-type').\r
+\r
+;; Similarly, since `false' and `null' are distinct in JSON, you can\r
+;; distinguish them by binding `json-false' and `json-null' as desired.\r
+\r
+;;; History:\r
+\r
+;; 2011-07-20 - Optimized by Austin Clements <aclements@csail.mit.edu>.\r
+;; 2006-03-11 - Initial version.\r
+;; 2006-03-13 - Added JSON generation in addition to parsing. Various\r
+;;              other cleanups, bugfixes, and improvements.\r
+;; 2006-12-29 - XEmacs support, from Aidan Kehoe <kehoea@parhasard.net>.\r
+;; 2008-02-21 - Installed in GNU Emacs.\r
+\r
+;;; Code:\r
+\r
+(eval-when-compile (require 'cl))\r
+\r
+;; Compatibility code\r
+\r
+(defalias 'json-encode-char0 'encode-char)\r
+(defalias 'json-decode-char0 'decode-char)\r
+\r
+\r
+;; Parameters\r
+\r
+(defvar json-object-type 'alist\r
+  "Type to convert JSON objects to.\r
+Must be one of `alist', `plist', or `hash-table'.  Consider let-binding\r
+this around your call to `json-read' instead of `setq'ing it.")\r
+\r
+(defvar json-array-type 'vector\r
+  "Type to convert JSON arrays to.\r
+Must be one of `vector' or `list'.  Consider let-binding this around\r
+your call to `json-read' instead of `setq'ing it.")\r
+\r
+(defvar json-key-type nil\r
+  "Type to convert JSON keys to.\r
+Must be one of `string', `symbol', `keyword', or nil.\r
+\r
+If nil, `json-read' will guess the type based on the value of\r
+`json-object-type':\r
+\r
+    If `json-object-type' is:   nil will be interpreted as:\r
+      `hash-table'                `string'\r
+      `alist'                     `symbol'\r
+      `plist'                     `keyword'\r
+\r
+Note that values other than `string' might behave strangely for\r
+Sufficiently Weird keys.  Consider let-binding this around your call to\r
+`json-read' instead of `setq'ing it.")\r
+\r
+(defvar json-false :json-false\r
+  "Value to use when reading JSON `false'.\r
+If this has the same value as `json-null', you might not be able to tell\r
+the difference between `false' and `null'.  Consider let-binding this\r
+around your call to `json-read' instead of `setq'ing it.")\r
+\r
+(defvar json-null nil\r
+  "Value to use when reading JSON `null'.\r
+If this has the same value as `json-false', you might not be able to\r
+tell the difference between `false' and `null'.  Consider let-binding\r
+this around your call to `json-read' instead of `setq'ing it.")\r
+\r
+\f\r
+\r
+\r
+;;; Utilities\r
+\r
+(defun json-join (strings separator)\r
+  "Join STRINGS with SEPARATOR."\r
+  (mapconcat 'identity strings separator))\r
+\r
+(defun json-alist-p (list)\r
+  "Non-null if and only if LIST is an alist."\r
+  (or (null list)\r
+      (and (consp (car list))\r
+           (json-alist-p (cdr list)))))\r
+\r
+(defun json-plist-p (list)\r
+  "Non-null if and only if LIST is a plist."\r
+  (or (null list)\r
+      (and (keywordp (car list))\r
+           (consp (cdr list))\r
+           (json-plist-p (cddr list)))))\r
+\r
+;; Reader utilities\r
+\r
+;; (defsubst json-advance (&optional n)\r
+;;   "Skip past the following N characters."\r
+;;   (forward-char n))\r
+\r
+(defalias 'json-advance 'forward-char)\r
+\r
+;; (defsubst json-peek ()\r
+;;   "Return the character at point."\r
+;;   (let ((char (char-after (point))))\r
+;;     (or char :json-eof)))\r
+\r
+(defsubst json-peek ()\r
+  "Return the character at point."\r
+  (or (char-after) :json-eof))\r
+\r
+(defsubst json-pop ()\r
+  "Advance past the character at point, returning it."\r
+  (let ((char (json-peek)))\r
+    (if (eq char :json-eof)\r
+        (signal 'end-of-file nil)\r
+      (json-advance)\r
+      char)))\r
+\r
+;; (defun json-skip-whitespace ()\r
+;;   "Skip past the whitespace at point."\r
+;;   (skip-chars-forward "\t\r\n\f\b "))\r
+\r
+(defsubst json-skip-whitespace ()\r
+  "Skip past the whitespace at point."\r
+  (skip-chars-forward "\t\r\n\f\b "))\r
+\r
+\f\r
+\r
+\r
+;; Error conditions\r
+\r
+(put 'json-error 'error-message "Unknown JSON error")\r
+(put 'json-error 'error-conditions '(json-error error))\r
+\r
+(put 'json-readtable-error 'error-message "JSON readtable error")\r
+(put 'json-readtable-error 'error-conditions\r
+     '(json-readtable-error json-error error))\r
+\r
+(put 'json-unknown-keyword 'error-message "Unrecognized keyword")\r
+(put 'json-unknown-keyword 'error-conditions\r
+     '(json-unknown-keyword json-error error))\r
+\r
+(put 'json-number-format 'error-message "Invalid number format")\r
+(put 'json-number-format 'error-conditions\r
+     '(json-number-format json-error error))\r
+\r
+(put 'json-string-escape 'error-message "Bad unicode escape")\r
+(put 'json-string-escape 'error-conditions\r
+     '(json-string-escape json-error error))\r
+\r
+(put 'json-string-format 'error-message "Bad string format")\r
+(put 'json-string-format 'error-conditions\r
+     '(json-string-format json-error error))\r
+\r
+(put 'json-object-format 'error-message "Bad JSON object")\r
+(put 'json-object-format 'error-conditions\r
+     '(json-object-format json-error error))\r
+\r
+\f\r
+\r
+\r
+;;; Keywords\r
+\r
+(defvar json-keywords '("true" "false" "null")\r
+  "List of JSON keywords.")\r
+\r
+;; Keyword parsing\r
+\r
+(defun json-read-keyword (keyword)\r
+  "Read a JSON keyword at point.\r
+KEYWORD is the keyword expected."\r
+  (unless (member keyword json-keywords)\r
+    (signal 'json-unknown-keyword (list keyword)))\r
+  (mapc (lambda (char)\r
+          (unless (char-equal char (json-peek))\r
+            (signal 'json-unknown-keyword\r
+                    (list (save-excursion\r
+                            (backward-word 1)\r
+                            (thing-at-point 'word)))))\r
+          (json-advance))\r
+        keyword)\r
+  (unless (looking-at "\\(\\s-\\|[],}]\\|$\\)")\r
+    (signal 'json-unknown-keyword\r
+            (list (save-excursion\r
+                    (backward-word 1)\r
+                    (thing-at-point 'word)))))\r
+  (cond ((string-equal keyword "true") t)\r
+        ((string-equal keyword "false") json-false)\r
+        ((string-equal keyword "null") json-null)))\r
+\r
+;; Keyword encoding\r
+\r
+(defun json-encode-keyword (keyword)\r
+  "Encode KEYWORD as a JSON value."\r
+  (cond ((eq keyword t)          "true")\r
+        ((eq keyword json-false) "false")\r
+        ((eq keyword json-null)  "null")))\r
+\r
+;;; Numbers\r
+\r
+;; Number parsing\r
+\r
+;; (defun json-read-number (&optional sign)\r
+;;  "Read the JSON number following point.\r
+;; The optional SIGN argument is for internal use.\r
+\r
+;; N.B.: Only numbers which can fit in Emacs Lisp's native number\r
+;; representation will be parsed correctly."\r
+;;  ;; If SIGN is non-nil, the number is explicitly signed.\r
+;;  (let ((number-regexp\r
+;;         "\\([0-9]+\\)?\\(\\.[0-9]+\\)?\\([Ee][+-]?[0-9]+\\)?"))\r
+;;    (cond ((and (null sign) (char-equal (json-peek) ?-))\r
+;;           (json-advance)\r
+;;           (- (json-read-number t)))\r
+;;          ((and (null sign) (char-equal (json-peek) ?+))\r
+;;           (json-advance)\r
+;;           (json-read-number t))\r
+;;          ((and (looking-at number-regexp)\r
+;;                (or (match-beginning 1)\r
+;;                    (match-beginning 2)))\r
+;;           (goto-char (match-end 0))\r
+;;           (string-to-number (match-string 0)))\r
+;;          (t (signal 'json-number-format (list (point)))))))\r
+\r
+(defun json-read-number ()\r
+ "Read the JSON number following point.\r
+\r
+N.B.: Only numbers which can fit in Emacs Lisp's native number\r
+representation will be parsed correctly."\r
+ ;; This regexp requires one character of backtrack in the common case\r
+ ;; of a whole number, but is slightly faster than a more explicit\r
+ ;; regexp like "\\([0-9]+\\)?\\(\\.[0-9]+\\)?"\r
+ (if (looking-at "[-+]?[0-9]*[.0-9][0-9]*\\([Ee][+-]?[0-9]+\\)?")\r
+     (progn\r
+       (goto-char (match-end 0))\r
+       (string-to-number (match-string 0)))\r
+   (signal 'json-number-format (list (point)))))\r
+\r
+;; Number encoding\r
+\r
+(defun json-encode-number (number)\r
+  "Return a JSON representation of NUMBER."\r
+  (format "%s" number))\r
+\r
+;;; Strings\r
+\r
+(defvar json-special-chars\r
+  '((?\" . ?\")\r
+    (?\\ . ?\\)\r
+    (?/ . ?/)\r
+    (?b . ?\b)\r
+    (?f . ?\f)\r
+    (?n . ?\n)\r
+    (?r . ?\r)\r
+    (?t . ?\t))\r
+  "Characters which are escaped in JSON, with their elisp counterparts.")\r
+\r
+;; String parsing\r
+\r
+(defun json-read-escaped-char ()\r
+  "Read the JSON string escaped character at point."\r
+  ;; Skip over the '\'\r
+  (json-advance)\r
+  (let* ((char (json-pop))\r
+         (special (assq char json-special-chars)))\r
+    (cond\r
+     (special (cdr special))\r
+     ((not (eq char ?u)) char)\r
+     ((looking-at "[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]")\r
+      (let ((hex (match-string 0)))\r
+        (json-advance 4)\r
+        (json-decode-char0 'ucs (string-to-number hex 16))))\r
+     (t\r
+      (signal 'json-string-escape (list (point)))))))\r
+\r
+;; (defun json-read-string ()\r
+;;   "Read the JSON string at point."\r
+;;   (unless (char-equal (json-peek) ?\")\r
+;;     (signal 'json-string-format (list "doesn't start with '\"'!")))\r
+;;   ;; Skip over the '"'\r
+;;   (json-advance)\r
+;;   (let ((characters '())\r
+;;         (char (json-peek)))\r
+;;     (while (not (char-equal char ?\"))\r
+;;       (push (if (char-equal char ?\\)\r
+;;                 (json-read-escaped-char)\r
+;;               (json-pop))\r
+;;             characters)\r
+;;       (setq char (json-peek)))\r
+;;     ;; Skip over the '"'\r
+;;     (json-advance)\r
+;;     (if characters\r
+;;         (apply 'string (nreverse characters))\r
+;;       "")))\r
+\r
+;; Really matters\r
+(defun json-read-string ()\r
+  "Read the JSON string at point."\r
+;;  (unless (char-equal (json-peek) ?\")\r
+;;    (signal 'json-string-format (list "doesn't start with '\"'!")))\r
+  ;; Skip over the '"'\r
+  (json-advance)\r
+  (let ((parts '()) (more t))\r
+    (while more\r
+      (let ((start (point)))\r
+       (when (> (skip-chars-forward "^\\\\\"") 0)\r
+         (push (buffer-substring-no-properties start (point)) parts)))\r
+      ;; Helps a little\r
+      (let ((char (char-after)))\r
+       (cond ((eq char ?\") (json-advance) (setq more nil))\r
+             ((eq char ?\\) (push (string (json-read-escaped-char)) parts))\r
+             (t (error "XXX Unterminated string")))))\r
+      ;; (let ((char (json-peek)))\r
+      ;;       (case char\r
+      ;;         ((?\") (json-advance) (setq done t))\r
+      ;;         ((?\\) (push (string (json-read-escaped-char)) parts))\r
+      ;;         (t     (error "XXX Unterminated string")))))\r
+    (if parts\r
+       (if (cdr parts)\r
+           (apply 'concat (nreverse parts))\r
+         (car parts))\r
+      "")))\r
+\r
+;; String encoding\r
+\r
+(defun json-encode-char (char)\r
+  "Encode CHAR as a JSON string."\r
+  (setq char (json-encode-char0 char 'ucs))\r
+  (let ((control-char (car (rassoc char json-special-chars))))\r
+    (cond\r
+     ;; Special JSON character (\n, \r, etc.)\r
+     (control-char\r
+      (format "\\%c" control-char))\r
+     ;; ASCIIish printable character\r
+     ((and (> char 31) (< char 161))\r
+      (format "%c" char))\r
+     ;; Fallback: UCS code point in \uNNNN form\r
+     (t\r
+      (format "\\u%04x" char)))))\r
+\r
+(defun json-encode-string (string)\r
+  "Return a JSON representation of STRING."\r
+  (format "\"%s\"" (mapconcat 'json-encode-char string "")))\r
+\r
+;;; JSON Objects\r
+\r
+(defun json-new-object ()\r
+  "Create a new Elisp object corresponding to a JSON object.\r
+Please see the documentation of `json-object-type'."\r
+  (cond ((eq json-object-type 'hash-table)\r
+         (make-hash-table :test 'equal))\r
+        (t\r
+         (list))))\r
+\r
+(defun json-add-to-object (object key value)\r
+  "Add a new KEY -> VALUE association to OBJECT.\r
+Returns the updated object, which you should save, e.g.:\r
+    (setq obj (json-add-to-object obj \"foo\" \"bar\"))\r
+Please see the documentation of `json-object-type' and `json-key-type'."\r
+  (let ((json-key-type\r
+         (if (eq json-key-type nil)\r
+             (cdr (assq json-object-type '((hash-table . string)\r
+                                           (alist . symbol)\r
+                                           (plist . keyword))))\r
+           json-key-type)))\r
+    (setq key\r
+          (cond ((eq json-key-type 'string)\r
+                 key)\r
+                ((eq json-key-type 'symbol)\r
+                 (intern key))\r
+                ((eq json-key-type 'keyword)\r
+                 (intern (concat ":" key)))))\r
+    (cond ((eq json-object-type 'hash-table)\r
+           (puthash key value object)\r
+           object)\r
+          ((eq json-object-type 'alist)\r
+           (cons (cons key value) object))\r
+          ((eq json-object-type 'plist)\r
+           (cons key (cons value object))))))\r
+\r
+;; JSON object parsing\r
+\r
+;; (defun json-read-object ()\r
+;;   "Read the JSON object at point."\r
+;;   ;; Skip over the "{"\r
+;;   (json-advance)\r
+;;   (json-skip-whitespace)\r
+;;   ;; read key/value pairs until "}"\r
+;;   (let ((elements (json-new-object))\r
+;;         key value)\r
+;;     (while (not (char-equal (json-peek) ?}))\r
+;;       (json-skip-whitespace)\r
+;;       (setq key (json-read-string))\r
+;;       (json-skip-whitespace)\r
+;;       (if (char-equal (json-peek) ?:)\r
+;;           (json-advance)\r
+;;         (signal 'json-object-format (list ":" (json-peek))))\r
+;;       (setq value (json-read))\r
+;;       (setq elements (json-add-to-object elements key value))\r
+;;       (json-skip-whitespace)\r
+;;       (unless (char-equal (json-peek) ?})\r
+;;         (if (char-equal (json-peek) ?,)\r
+;;             (json-advance)\r
+;;           (signal 'json-object-format (list "," (json-peek))))))\r
+;;     ;; Skip over the "}"\r
+;;     (json-advance)\r
+;;     elements))\r
+\r
+\r
+(defun json-read-object ()\r
+  "Read the JSON object at point."\r
+  ;; Skip over the "{"\r
+  (json-advance)\r
+  (json-skip-whitespace)\r
+  ;; read key/value pairs until "}"\r
+  (let ((elements (json-new-object))\r
+        key value (more t))\r
+    (unless (eq (char-after) ?})\r
+;;    (while (not (eq (char-after) ?}))\r
+      (while more\r
+       (unless (eq (char-after) ?\")\r
+         (json-skip-whitespace)\r
+         (unless (eq (char-after) ?\")\r
+           (signal 'json-string-format (list "doesn't start with '\"'!"))))\r
+      (setq key (json-read-string))\r
+      ;; Makes a small but surprising difference, adds up if done\r
+      ;; consistently\r
+      (if (eq (char-after) ?:)\r
+          (json-advance)\r
+       (if (progn (json-skip-whitespace) (eq (char-after) ?:))\r
+           (json-advance)\r
+         (signal 'json-object-format (list ":" (json-peek)))))\r
+      (setq value (json-read))\r
+      (setq elements (json-add-to-object elements key value))\r
+      ;; Order matters a little\r
+      (cond ((eq (char-after) ?,) (json-advance))\r
+           ((eq (char-after) ?}) (setq more nil))\r
+           ((progn\r
+              (json-skip-whitespace)\r
+              (eq (char-after) ?,)) (json-advance))\r
+           ((eq (char-after) ?}) (setq more nil))\r
+           (t (signal 'json-object-format (list "," (json-peek)))))))\r
+      ;; (unless (char-equal (json-peek) ?})\r
+      ;;   (if (char-equal (json-peek) ?,)\r
+      ;;       (json-advance)\r
+      ;;     (signal 'json-object-format (list "," (json-peek))))))\r
+    ;; Skip over the "}"\r
+    (json-advance)\r
+    elements))\r
+\r
+;; Hash table encoding\r
+\r
+(defun json-encode-hash-table (hash-table)\r
+  "Return a JSON representation of HASH-TABLE."\r
+  (format "{%s}"\r
+          (json-join\r
+           (let (r)\r
+             (maphash\r
+              (lambda (k v)\r
+                (push (format "%s:%s"\r
+                              (json-encode k)\r
+                              (json-encode v))\r
+                      r))\r
+              hash-table)\r
+             r)\r
+           ", ")))\r
+\r
+;; List encoding (including alists and plists)\r
+\r
+(defun json-encode-alist (alist)\r
+  "Return a JSON representation of ALIST."\r
+  (format "{%s}"\r
+          (json-join (mapcar (lambda (cons)\r
+                               (format "%s:%s"\r
+                                       (json-encode (car cons))\r
+                                       (json-encode (cdr cons))))\r
+                             alist)\r
+                     ", ")))\r
+\r
+(defun json-encode-plist (plist)\r
+  "Return a JSON representation of PLIST."\r
+  (let (result)\r
+    (while plist\r
+      (push (concat (json-encode (car plist))\r
+                    ":"\r
+                    (json-encode (cadr plist)))\r
+            result)\r
+      (setq plist (cddr plist)))\r
+    (concat "{" (json-join (nreverse result) ", ") "}")))\r
+\r
+(defun json-encode-list (list)\r
+  "Return a JSON representation of LIST.\r
+Tries to DWIM: simple lists become JSON arrays, while alists and plists\r
+become JSON objects."\r
+  (cond ((null list)         "null")\r
+        ((json-alist-p list) (json-encode-alist list))\r
+        ((json-plist-p list) (json-encode-plist list))\r
+        ((listp list)        (json-encode-array list))\r
+        (t\r
+         (signal 'json-error (list list)))))\r
+\r
+;;; Arrays\r
+\r
+;; Array parsing\r
+\r
+;; (defun json-read-array ()\r
+;;   "Read the JSON array at point."\r
+;;   ;; Skip over the "["\r
+;;   (json-advance)\r
+;;   (json-skip-whitespace)\r
+;;   ;; read values until "]"\r
+;;   (let (elements)\r
+;;     (while (not (char-equal (json-peek) ?\]))\r
+;;       (push (json-read) elements)\r
+;;       (json-skip-whitespace)\r
+;;       (unless (char-equal (json-peek) ?\])\r
+;;         (if (char-equal (json-peek) ?,)\r
+;;             (json-advance)\r
+;;           (signal 'json-error (list 'bleah)))))\r
+;;     ;; Skip over the "]"\r
+;;     (json-advance)\r
+;;     (apply json-array-type (nreverse elements))))\r
+\r
+(defun json-read-array ()\r
+  "Read the JSON array at point."\r
+  ;; Skip over the "["\r
+  (json-advance)\r
+  (json-skip-whitespace)\r
+  ;; read values until "]"\r
+  (let* (elements (more t))\r
+    (unless (eq (char-after) ?\])\r
+      (while more\r
+;;    (while (not (char-equal (json-peek) ?\]))\r
+      (push (json-read) elements)\r
+      ;; Doesn't help\r
+;;     (setq tail (setcdr tail (cons (json-read) nil)))\r
+\r
+;;      (json-skip-whitespace)\r
+      (cond ((eq (char-after) ?,) (json-advance))\r
+           ((eq (char-after) ?\]) (setq more nil))\r
+           ((progn\r
+              (json-skip-whitespace)\r
+              (eq (char-after) ?,)) (json-advance))\r
+           ((eq (char-after) ?\]) (setq more nil))\r
+           (t (signal 'json-error (list 'bleah))))))\r
+      ;; (unless (char-equal (json-peek) ?\])\r
+      ;;   (if (char-equal (json-peek) ?,)\r
+      ;;       (json-advance)\r
+      ;;     (signal 'json-error (list 'bleah))))))\r
+    ;; Skip over the "]"\r
+    (json-advance)\r
+    ;; Matters\r
+    (if (eq json-array-type 'list)\r
+       (nreverse elements)\r
+      (apply json-array-type (nreverse elements)))))\r
+\r
+;; Array encoding\r
+\r
+(defun json-encode-array (array)\r
+  "Return a JSON representation of ARRAY."\r
+  (concat "[" (mapconcat 'json-encode array ", ") "]"))\r
+\r
+\f\r
+\r
+\r
+;;; JSON reader.\r
+\r
+;; (defvar json-readtable\r
+;;   (let ((table\r
+;;          '((?t json-read-keyword "true")\r
+;;            (?f json-read-keyword "false")\r
+;;            (?n json-read-keyword "null")\r
+;;            (?{ json-read-object)\r
+;;            (?\[ json-read-array)\r
+;;            (?\" json-read-string))))\r
+;;     (mapc (lambda (char)\r
+;;             (push (list char 'json-read-number) table))\r
+;;           '(?- ?+ ?. ?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9))\r
+;;     table)\r
+;;   "Readtable for JSON reader.")\r
+\r
+;; (defun json-read ()\r
+;;   "Parse and return the JSON object following point.\r
+;; Advances point just past JSON object."\r
+;;   (json-skip-whitespace)\r
+;;   (let ((char (json-peek)))\r
+;;     (if (not (eq char :json-eof))\r
+;;         (let ((record (cdr (assq char json-readtable))))\r
+;;           (if (functionp (car record))\r
+;;               (apply (car record) (cdr record))\r
+;;             (signal 'json-readtable-error record)))\r
+;;       (signal 'end-of-file nil))))\r
+\r
+(defvar my-json-readtable\r
+  (let ((table (make-char-table nil)))\r
+    (aset table ?t '(json-read-keyword "true"))\r
+    (aset table ?f '(json-read-keyword "false"))\r
+    (aset table ?n '(json-read-keyword "null"))\r
+    (aset table ?{ '(json-read-object))\r
+    (aset table ?\[ '(json-read-array))\r
+    (aset table ?\" '(json-read-string))\r
+    (mapc (lambda (char)\r
+           (aset table char '(json-read-number)))\r
+         '(?- ?+ ?. ?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9))\r
+    table)\r
+  "Readtable for JSON reader.")\r
+\r
+;; Char-table matters a bit; (if (null ..)) matters more\r
+;; (defun json-read ()\r
+;;   "Parse and return the JSON object following point.\r
+;; Advances point just past JSON object."\r
+;;   (json-skip-whitespace)\r
+;;   (let ((char (json-peek)))\r
+;;     (if (not (eq char :json-eof))\r
+;;     (let ((record (aref my-json-readtable char)))\r
+;;       (if (null (car record))\r
+;;           (signal 'json-readtable-error record)\r
+;;         (apply (car record) (cdr record))))\r
+;;       (signal 'end-of-file nil))))\r
+\r
+;; Makes no difference or slower, probably because there's usually whitespace\r
+;; (defun json-read ()\r
+;;   "Parse and return the JSON object following point.\r
+;; Advances point just past JSON object."\r
+;;   (let ((record (and (char-after) (aref my-json-readtable (char-after)))))\r
+;;     (when (null record)\r
+;;       (json-skip-whitespace)\r
+;;       (when (eobp)\r
+;;     (signal 'end-of-file nil))\r
+;;       (setq record (aref my-json-readtable (char-after)))\r
+;;       (when (null record)\r
+;;     (signal 'json-readtable-error record)))\r
+;;     (apply (car record) (cdr record))))\r
+\r
+;; Makes a difference\r
+(defun json-read ()\r
+  "Parse and return the JSON object following point.\r
+Advances point just past JSON object."\r
+  (json-skip-whitespace)\r
+  (if (char-after)\r
+      (let ((record (aref my-json-readtable (char-after))))\r
+       (if record\r
+           (apply (car record) (cdr record))\r
+         (signal 'json-readtable-error record)))\r
+    (signal 'end-of-file nil)))\r
+\r
+;; Syntactic sugar for the reader\r
+\r
+(defun json-read-from-string (string)\r
+  "Read the JSON object contained in STRING and return it."\r
+  (with-temp-buffer\r
+    (insert string)\r
+    (goto-char (point-min))\r
+    (json-read)))\r
+\r
+(defun json-read-file (file)\r
+  "Read the first JSON object contained in FILE and return it."\r
+  (with-temp-buffer\r
+    (insert-file-contents file)\r
+    (goto-char (point-min))\r
+    (json-read)))\r
+\r
+\f\r
+\r
+\r
+;;; JSON encoder\r
+\r
+(defun json-encode (object)\r
+  "Return a JSON representation of OBJECT as a string."\r
+  (cond ((memq object (list t json-null json-false))\r
+         (json-encode-keyword object))\r
+        ((stringp object)      (json-encode-string object))\r
+        ((keywordp object)     (json-encode-string\r
+                                (substring (symbol-name object) 1)))\r
+        ((symbolp object)      (json-encode-string\r
+                                (symbol-name object)))\r
+        ((numberp object)      (json-encode-number object))\r
+        ((arrayp object)       (json-encode-array object))\r
+        ((hash-table-p object) (json-encode-hash-table object))\r
+        ((listp object)        (json-encode-list object))\r
+        (t                     (signal 'json-error (list object)))))\r
+\r
+(provide 'json)\r
+\r
+;; arch-tag: 15f6e4c8-b831-4172-8749-bbc680c50ea1\r
+;;; json.el ends here\r
+\r
+--/04w6evG8XlLl3ft--\r