Add some limited JSON Patch support for board description files.

Boards can now be provided as .patch files that are JSON documents
containing two nodes:

 - `base` is the board that this is based on, which can be another
   .patch file, a full path (note: relative paths are not yet supported,
   but they should be for out-of-tree BSPs).
 - `patch` is a JSON Patch.

The patch is a subset of RFC 6902.  Current limitations:

 - It doesn't handle escapes in keys or any weird keys.
 - It assumes that numbers in the JSON Pointer path are array indexes
   and will do the wrong thing if you try to replace or add an object
   field with a number as a key.
 - It doesn't implement the copy, move, or test operations, only add,
   remove, and replace.

This is sufficient to be useful.  The Arty A7 and Sonata Simulator JSON
files are now replaced with patches.
diff --git a/sdk/boards/ibex-arty-a7-100.json b/sdk/boards/ibex-arty-a7-100.json
deleted file mode 100644
index db54172..0000000
--- a/sdk/boards/ibex-arty-a7-100.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
-    "devices": {
-        "clint": {
-            "start": 0x14001000,
-            "length": 0x1000
-        },
-        "plic": {
-            "start": 0x10000000,
-            "end": 0x10400000
-        },
-        "revoker": {
-            "start": 0x14000000,
-            "length": 0x1000
-        },
-        "uart": {
-            "start": 0x8f00b000,
-            "end":   0x8f00b100
-        },
-        "shadow" : {
-            "start": 0x200fe000,
-            "length": 0x2000
-        },
-        "gpio_led0" : {
-            "start": 0x8f00f000,
-            "length": 0x800
-        },
-        "kunyan_ethernet": {
-            "start": 0x14004000,
-            "end": 0x14008000
-        }
-    },
-    "instruction_memory": {
-        "start": 0x20040000,
-        "end": 0x20080000
-    },
-    "heap": {
-        "end": 0x20080000
-    },
-    "interrupts": [
-        {
-            "name": "RevokerInterrupt",
-            "number": 1,
-            "priority": 2
-        },
-        {
-            "name": "UARTInterrupt",
-            "number": 2,
-            "priority": 3,
-            "edge_triggered": true
-        },
-        {
-            "name": "EthernetTransmitInterrupt",
-            "number": 3,
-            "priority": 3
-        },
-        {
-            "name": "EthernetReceiveInterrupt",
-            "number": 4,
-            "priority": 3
-        }
-    ],
-    "defines" : [
-        "IBEX",
-        "IBEX_SAFE"
-    ],
-    "driver_includes" : [
-        "${sdk}/include/platform/arty-a7",
-        "${sdk}/include/platform/synopsis",
-        "${sdk}/include/platform/ibex",
-        "${sdk}/include/platform/generic-riscv"
-    ],
-    "timer_hz" : 33000000,
-    "tickrate_hz" : 100,
-    "revoker" : "hardware",
-    "stack_high_water_mark" : true
-}
diff --git a/sdk/boards/ibex-arty-a7-100.patch b/sdk/boards/ibex-arty-a7-100.patch
new file mode 100644
index 0000000..3170507
--- /dev/null
+++ b/sdk/boards/ibex-arty-a7-100.patch
@@ -0,0 +1,77 @@
+{
+  "base": "ibex-safe-simulator",
+  "patch": [
+    {
+      "op": "add",
+      "path": "/devices/gpio_led0",
+      "value": {
+        "start": 0x8f00f000,
+        "length": 0x800
+      }
+    },
+    {
+      "op": "add",
+      "path": "/kunyan_ethernet",
+      "value": {
+        "start": 0x14004000,
+        "end": 0x14008000
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/1",
+      "value": {
+        "name": "UARTInterrupt",
+        "number": 2,
+        "priority": 3,
+        "edge_triggered": true
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/2",
+      "value": {
+        "name": "EthernetTransmitInterrupt",
+        "number": 3,
+        "priority": 3
+      }
+    },
+    {
+      "op": "add",
+      "path": "/interrupts/3",
+      "value": {
+        "name": "EthernetReceiveInterrupt",
+        "number": 4,
+        "priority": 3
+      }
+    },
+    {
+      "op": "add",
+      "path": "/driver_includes/0",
+      "value": "${sdk}/include/platform/synopsis"
+    },
+    {
+      "op": "add",
+      "path": "/driver_includes/0",
+      "value": "${sdk}/include/platform/arty-a7"
+    },
+    {
+      "op": "replace",
+      "path": "/timer_hz",
+      "value": 33000000
+    },
+    {
+      "op": "replace",
+      "path": "/tickrate_hz",
+      "value": 100
+    },
+    {
+      "op": "remove",
+      "path": "/simulation"
+    },
+    {
+      "op": "remove",
+      "path": "/simulator"
+    }
+  ]
+}
diff --git a/sdk/boards/sonata-simulator.json b/sdk/boards/sonata-simulator.json
deleted file mode 100644
index 2573a40..0000000
--- a/sdk/boards/sonata-simulator.json
+++ /dev/null
@@ -1,144 +0,0 @@
-{
-    "devices": {
-        "shadow" : {
-            "start" : 0x30000000,
-            "end"   : 0x30000800
-        },
-        "pwm": {
-            "start" : 0x80001000,
-            "length": 0x00001000
-        },
-        "rgbled" : {
-            "start" : 0x80009000,
-            "end"   : 0x80009020
-        },
-        "revoker": {
-            "start" : 0x8000A000,
-            "length": 0x00001000
-        },
-        "adc": {
-            "start" : 0x8000B000,
-            "length": 0x00001000
-        },
-        "clint": {
-            "start" : 0x80040000,
-            "end"   : 0x80050000
-        },
-        "uart": {
-            "start" : 0x80100000,
-            "end"   : 0x80100034
-        },
-        "uart1": {
-            "start" : 0x80101000,
-            "end"   : 0x80101034
-        },
-        "uart2": {
-            "start" : 0x80102000,
-            "end"   : 0x80102034
-        },
-        "usbdev": {
-            "start" : 0x80400000,
-            "end"   : 0x80401000
-        },
-        "plic": {
-            "start" : 0x88000000,
-            "end"   : 0x88400000
-        }
-    },
-    "instruction_memory": {
-        "start": 0x00101000,
-        "end":   0x00120000
-    },
-    "heap": {
-        "end": 0x00120000
-    },
-    "revokable_memory_start": 0x00100000,
-    "defines" : [
-        "IBEX",
-        "SUNBURST",
-        "SUNBURST_SHADOW_BASE=0x30000000",
-        "SUNBURST_SHADOW_SIZE=0x800",
-        "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1",
-        "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1"
-    ],
-    "cxflags": "-mllvm -enable-machine-outliner=never",
-    "driver_includes" : [
-        "../include/platform/sunburst",
-        "../include/platform/ibex",
-        "../include/platform/generic-riscv"
-    ],
-    "timer_hz" : 40000000,
-    "tickrate_hz" : 100,
-    "revoker" : "hardware",
-    "stack_high_water_mark" : true,
-    "simulator" : "${sdk}/../scripts/run-sonata-sim.sh",
-    "simulation": true,
-    "interrupts": [
-        {
-            "name": "RevokerInterrupt",
-            "number": 1,
-            "priority": 2,
-            "edge_triggered": true
-        },
-        {
-            "name": "EthernetInterrupt",
-            "number": 2,
-            "priority": 3
-        },
-        {
-            "name": "UsbDevInterrupt",
-            "number": 3,
-            "priority": 3
-        },
-        {
-            "name": "Uart0Interrupt",
-            "number": 8,
-            "priority": 3
-        },
-        {
-            "name": "Uart1Interrupt",
-            "number": 9,
-            "priority": 3
-        },
-        {
-            "name": "Uart2Interrupt",
-            "number": 10,
-            "priority": 3
-        },
-        {
-            "name": "I2c0Interrupt",
-            "number": 16,
-            "priority": 3
-        },
-        {
-            "name": "I2c1Interrupt",
-            "number": 17,
-            "priority": 3
-        },
-        {
-            "name": "SpiLcdInterrupt",
-            "number": 24,
-            "priority": 3
-        },
-        {
-            "name": "SpiEthmacInterrupt",
-            "number": 25,
-            "priority": 3
-        },
-        {
-            "name": "Spi0Interrupt",
-            "number": 26,
-            "priority": 3
-        },
-        {
-            "name": "Spi1Interrupt",
-            "number": 27,
-            "priority": 3
-        },
-        {
-            "name": "Spi2Interrupt",
-            "number": 28,
-            "priority": 3
-        }
-    ]
-}
diff --git a/sdk/boards/sonata-simulator.patch b/sdk/boards/sonata-simulator.patch
new file mode 100644
index 0000000..650e419
--- /dev/null
+++ b/sdk/boards/sonata-simulator.patch
@@ -0,0 +1,15 @@
+{
+  "base": "sonata-prerelease",
+  "patch": [
+    {
+      "op": "replace",
+      "path": "/simulation",
+      "value": true
+    },
+    {
+      "op": "replace",
+      "path": "/simulator",
+      "value": "${sdk}/../scripts/run-sonata-sim.sh"
+    }
+  ]
+}
diff --git a/sdk/xmake.lua b/sdk/xmake.lua
index 0d02151..2ea30e5 100644
--- a/sdk/xmake.lua
+++ b/sdk/xmake.lua
@@ -253,21 +253,143 @@
 		target:set("cheriot.ldscript", "software_revoker.ldscript")
 	end)
 
--- Helper to get the board file for a given target
-local board_file = function(target)
-	local boardfile = target:values("board")
-	if not boardfile then
-		raise("target " .. target:name() .. " does not define a board name")
-	end
+-- Helper to find a board file given either the name of a board file or a path.
+local function board_file_for_name(boardName)
+	local boardfile = boardName
 	-- The directory containing the board file.
 	local boarddir = path.directory(boardfile);
+	-- If this isn't a path, look in the boards directory
 	if path.basename(boardfile) == boardfile then
 		boarddir = path.join(scriptdir, "boards")
-		boardfile = path.join(boarddir, boardfile .. '.json')
+		local fullBoardPath = path.join(boarddir, boardfile .. '.json')
+		if not os.isfile(fullBoardPath) then
+			fullBoardPath = path.join(boarddir, boardfile .. '.patch')
+		end
+		if not os.isfile(fullBoardPath) then
+			print("unable to find board file " .. boardfile .. ".  Try specifying a full path")
+			return nil
+		end
+		boardfile = fullBoardPath
 	end
 	return boarddir, boardfile
 end
 
+-- Helper to get the board file for a given target
+local function board_file_for_target(target)
+	local boardName = target:values("board")
+	if not boardName then
+		print("target " .. target:name() .. " does not define a board name")
+		return nil
+	end
+	return board_file_for_name(boardName)
+end
+
+-- Helper to load a board file.  This must be passed the json object provided
+-- by import("core.base.json") because import does not work in helper
+-- functions at the top level.
+local function load_board_file(json, boardFile)
+	if path.extension(boardFile) == ".json" then
+		return json.loadfile(boardFile)
+	end
+	if path.extension(boardFile) ~= ".patch" then
+		print("unknown extension for board file: " .. boardFile)
+		return nil
+	end
+	local patch = json.loadfile(boardFile)
+	if not patch.base then
+		print("Board file " .. boardFile .. " does not specify a base")
+		return nil
+	end
+	local _, baseFile = board_file_for_name(patch.base)
+	local base = load_board_file(json, baseFile)
+
+	-- If a string value is a number, return it as number, otherwise return it
+	-- in its original form.
+	function asNumberIfNumber(value)
+		if tostring(tonumber(value)) == value then
+			return tonumber(value)
+		end
+		return value
+	end
+
+	-- Heuristic to tell a Lua table is probably an array in Lua
+	-- This is O(n), but n is usually very small, and this happens once per
+	-- build so this doesn't really matter.
+	function isarray(t)
+		local i = 1
+		for k, v in pairs(t) do
+			if k ~= i then
+				return false
+			end
+			i = i+1
+		end
+		return true
+	end
+
+	for _, p in ipairs(patch.patch) do
+		if not p.op then
+			print("missing op in "..json.encode(p))
+			return nil
+		end
+		if not p.path or (type(p.path) ~= "string") then
+			print("missing or invalid path in "..json.encode(p))
+			return nil
+		end
+
+		-- Parse the JSON Pointer into an array of filed names, converting
+		-- numbers into Lua numbers if we see them.  This is not quite right,
+		-- because it doesn't handle field names with / in them, but we don't
+		-- currently use those for anything.  It also assumes that we really do
+		-- mean array indexes when we say numbers.  If we have an object with
+		-- "3" as the key and try to replace 3, it will currently not do the
+		-- right thing.  
+		local objectPath = {}
+		for entry in string.gmatch(p.path, "/([^/]+)") do
+			table.insert(objectPath, asNumberIfNumber(entry))
+		end
+
+		if #objectPath < 1 then
+			print("invalid path in "..json.encode(p))
+			return nil
+		end
+
+		-- Last path object is the name of the key we're going to modify.
+		local nodeName = table.remove(objectPath)
+		-- Walk the path to find the object that we're going to modify.
+		local nodeToModify = base
+		for _, pathComponent in ipairs(objectPath) do
+			nodeToModify = nodeToModify[pathComponent]
+		end
+
+		-- JSON arrays are indexed from 0, Lua's are from 1.  If someone says
+		-- array index 0, we need to map that to 1, and so on.
+		local isArrayOperation = false
+		if (type(nodeName) == "number") and isarray(nodeToModify) then
+			nodeName = nodeName + 1
+			isArrayOperation = true
+		end
+
+		-- Handle the operation
+		if (p.op == "replace") or (p.op == "add") then
+			if not p.value then
+				print(tostring(p.op).. " requires a value, missing in ", json.encode(p))
+				return nil
+			end
+			if isArrayOperation and p.op == "add" then
+				table.insert(nodeToModify, nodeName, p.value)
+			else
+				nodeToModify[nodeName] = p.value
+			end
+		elseif p.op == "remove" then
+			nodeToModify[nodeName] = nil
+		else
+			print(tostring(p.op) .. " is not a valid operation in ", json.encode(p))
+			return nil
+		end
+	end
+	return base
+end
+
 -- Helper to visit all dependencies of a specified target exactly once and call
 -- a callback.
 local function visit_all_dependencies_of(target, callback)
@@ -284,14 +406,13 @@
 	visit(target)
 end
 
-
 -- Rule for defining a firmware image.
 rule("firmware")
 	on_run(function (target)
 		import("core.base.json")
 		import("core.project.config")
-		local boarddir, boardfile = board_file(target)
-		local board = json.loadfile(boardfile)
+		local boarddir, boardfile = board_file_for_target(target)
+		local board = load_board_file(json, boardfile)
 		if not board.simulator then
 			raise("board description " .. boardfile .. " does not define a run command")
 		end
@@ -339,9 +460,10 @@
 			visit_all_dependencies_of(target, callback)
 		end
 
-		local boarddir, boardfile = board_file(target);
-		local board = json.loadfile(boardfile)
-
+		local boarddir, boardfile = board_file_for_target(target);
+		local board = load_board_file(json, boardfile)
+		print("Board file saved as ", target:targetfile()..".board.json")
+		json.savefile(target:targetfile()..".board.json", board)
 
 		-- Add defines to all dependencies.
 		local add_defines = function (defines)