From 9d519cdd00b0e2d38198dc81c3cd75f3b152843b Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Tue, 23 May 2017 12:55:20 -0400
Subject: [PATCH 34/48] LSP prototype v2

---
 gcc/Makefile.in                             |  23 +-
 gcc/c/c-decl.cc                             |  88 ++
 gcc/c/c-parser.cc                           |  10 +
 gcc/common.opt                              |  16 +
 gcc/configure.ac                            |   2 +-
 gcc/diagnostics/color.cc                    |   6 +-
 gcc/diagnostics/digraphs-to-dot-from-cfg.cc |   8 +-
 gcc/diagnostics/digraphs.cc                 |   8 +-
 gcc/diagnostics/digraphs.h                  |   2 +-
 gcc/json.cc                                 |  11 +
 gcc/json.h                                  |  18 +-
 gcc/lsp/README.txt                          |   9 +
 gcc/lsp/frontend-interface.h                |  47 +
 gcc/lsp/gcc-impl/common.cc                  |  45 +
 gcc/lsp/gcc-impl/common.h                   |  37 +
 gcc/lsp/gcc-impl/server.cc                  | 687 ++++++++++++++
 gcc/lsp/gcc-impl/server.h                   | 310 +++++++
 gcc/lsp/gcc-impl/toplev.cc                  | 118 +++
 gcc/lsp/gcc-impl/toplev.h                   |  41 +
 gcc/lsp/gcc-impl/worker-diagnostics.cc      | 416 +++++++++
 gcc/lsp/gcc-impl/worker-diagnostics.h       | 163 ++++
 gcc/lsp/gcc-impl/worker.cc                  | 404 +++++++++
 gcc/lsp/gcc-impl/worker.h                   | 140 +++
 gcc/lsp/ipc.cc                              | 343 +++++++
 gcc/lsp/ipc.h                               | 256 ++++++
 gcc/lsp/json-rpc.cc                         | 616 +++++++++++++
 gcc/lsp/json-rpc.h                          | 305 +++++++
 gcc/lsp/lsp-logging.cc                      | 189 ++++
 gcc/lsp/lsp-logging.h                       | 202 +++++
 gcc/lsp/lsp-spec.cc                         | 947 ++++++++++++++++++++
 gcc/lsp/lsp-spec.h                          | 591 ++++++++++++
 gcc/lsp/lsp-to-octets.h                     |  59 ++
 gcc/lsp/mini-http.cc                        | 389 ++++++++
 gcc/lsp/mini-http.h                         | 235 +++++
 gcc/lsp/octets-to-json-rpc.cc               |  76 ++
 gcc/lsp/octets-to-json-rpc.h                |  53 ++
 gcc/lsp/octets.cc                           |  72 ++
 gcc/lsp/octets.h                            | 117 +++
 gcc/lsp/subprocess.cc                       | 207 +++++
 gcc/lsp/subprocess.h                        |  75 ++
 gcc/opts-diagnostic.cc                      |   2 +-
 gcc/selftest-run-tests.cc                   |  11 +-
 gcc/selftest.h                              |   7 +
 gcc/testsuite/gcc.dg/lsp/cli-client.py      | 270 ++++++
 gcc/testsuite/gcc.dg/lsp/lsp.py             | 239 +++++
 gcc/testsuite/gcc.dg/lsp/test.c             |  12 +
 gcc/testsuite/gcc.dg/lsp/test.py            |  28 +
 gcc/testsuite/gcc.dg/lsp/toy-ide.py         | 182 ++++
 gcc/toplev.cc                               |  22 +
 gcc/uris.cc                                 | 180 ++++
 gcc/uris.h                                  |  46 +
 libcpp/files.cc                             |  28 +
 libcpp/include/cpplib.h                     |   4 +
 53 files changed, 8350 insertions(+), 22 deletions(-)
 create mode 100644 gcc/lsp/README.txt
 create mode 100644 gcc/lsp/frontend-interface.h
 create mode 100644 gcc/lsp/gcc-impl/common.cc
 create mode 100644 gcc/lsp/gcc-impl/common.h
 create mode 100644 gcc/lsp/gcc-impl/server.cc
 create mode 100644 gcc/lsp/gcc-impl/server.h
 create mode 100644 gcc/lsp/gcc-impl/toplev.cc
 create mode 100644 gcc/lsp/gcc-impl/toplev.h
 create mode 100644 gcc/lsp/gcc-impl/worker-diagnostics.cc
 create mode 100644 gcc/lsp/gcc-impl/worker-diagnostics.h
 create mode 100644 gcc/lsp/gcc-impl/worker.cc
 create mode 100644 gcc/lsp/gcc-impl/worker.h
 create mode 100644 gcc/lsp/ipc.cc
 create mode 100644 gcc/lsp/ipc.h
 create mode 100644 gcc/lsp/json-rpc.cc
 create mode 100644 gcc/lsp/json-rpc.h
 create mode 100644 gcc/lsp/lsp-logging.cc
 create mode 100644 gcc/lsp/lsp-logging.h
 create mode 100644 gcc/lsp/lsp-spec.cc
 create mode 100644 gcc/lsp/lsp-spec.h
 create mode 100644 gcc/lsp/lsp-to-octets.h
 create mode 100644 gcc/lsp/mini-http.cc
 create mode 100644 gcc/lsp/mini-http.h
 create mode 100644 gcc/lsp/octets-to-json-rpc.cc
 create mode 100644 gcc/lsp/octets-to-json-rpc.h
 create mode 100644 gcc/lsp/octets.cc
 create mode 100644 gcc/lsp/octets.h
 create mode 100644 gcc/lsp/subprocess.cc
 create mode 100644 gcc/lsp/subprocess.h
 create mode 100644 gcc/testsuite/gcc.dg/lsp/cli-client.py
 create mode 100644 gcc/testsuite/gcc.dg/lsp/lsp.py
 create mode 100644 gcc/testsuite/gcc.dg/lsp/test.c
 create mode 100644 gcc/testsuite/gcc.dg/lsp/test.py
 create mode 100644 gcc/testsuite/gcc.dg/lsp/toy-ide.py
 create mode 100644 gcc/uris.cc
 create mode 100644 gcc/uris.h

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index b00e4679447..4072deb8100 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1392,6 +1392,22 @@ ANALYZER_OBJS = \
 	analyzer/trimmed-graph.o \
 	analyzer/varargs.o
 
+# FIXME
+LSP_OBJS = \
+	lsp/gcc-impl/server.o \
+	lsp/gcc-impl/worker.o \
+	lsp/gcc-impl/worker-diagnostics.o \
+	lsp/gcc-impl/common.o \
+	lsp/gcc-impl/toplev.o \
+	lsp/ipc.o \
+	lsp/octets.o \
+	lsp/octets-to-json-rpc.o \
+	lsp/mini-http.o \
+	lsp/json-rpc.o \
+	lsp/lsp-logging.o \
+	lsp/lsp-spec.o \
+	lsp/subprocess.o
+
 # Language-independent object files.
 # We put the *-match.o and insn-*.o files first so that a parallel make
 # will build them sooner, because they are large and otherwise tend to be
@@ -1869,6 +1885,7 @@ OBJS = \
 	attr-callback.o \
 	$(out_object_file) \
 	$(ANALYZER_OBJS) \
+	$(LSP_OBJS) \
 	$(EXTRA_OBJS) \
 	$(host_hook_obj)
 
@@ -1924,7 +1941,8 @@ OBJS-libcommon = \
 	text-art/table.o \
 	text-art/theme.o \
 	text-art/tree-widget.o \
-	text-art/widget.o
+	text-art/widget.o \
+	uris.o
 
 # Objects in libcommon-target.a, used by drivers and by the core
 # compiler and containing target-dependent code.
@@ -4170,7 +4188,7 @@ PLUGIN_HEADERS = $(TREE_H) $(CONFIG_H) $(SYSTEM_H) coretypes.h $(TM_H) \
   lcm.h cfgloopmanip.h file-prefix-map.h builtins.def $(INSN_ATTR_H) \
   pass-instances.def params.list $(srcdir)/../include/gomp-constants.h \
   $(EXPR_H) $(srcdir)/analyzer/*.h $(srcdir)/diagnostics/*.h \
-  $(srcdir)/text-art/*.h
+  $(srcdir)/text-art/*.h $(srcdir)/lsp/*.h
 
 # generate the 'build fragment' b-header-vars
 s-header-vars: Makefile
@@ -4209,6 +4227,7 @@ install-plugin: installdirs lang.install-plugin s-header-vars install-gengtype
 	  case $$path in \
 	  "$(srcdir)"/analyzer/* \
 	  | "$(srcdir)"/diagnostics/* \
+	  | "$(srcdir)"/lsp/* \
 	  | "$(srcdir)"/text-art/* \
 	  | "$(srcdir)"/config/* | "$(srcdir)"/common/config/* \
 	  | "$(srcdir)"/c-family/* | "$(srcdir)"/*.def ) \
diff --git a/gcc/c/c-decl.cc b/gcc/c/c-decl.cc
index 98249b92269..8c7679c9895 100644
--- a/gcc/c/c-decl.cc
+++ b/gcc/c/c-decl.cc
@@ -25,7 +25,10 @@ along with GCC; see the file COPYING3.  If not see
    line numbers.  For example, the CONST_DECLs for enum values.  */
 
 #include "config.h"
+#define INCLUDE_LIST
+#define INCLUDE_MAP
 #define INCLUDE_STRING
+#define INCLUDE_VECTOR
 #include "system.h"
 #include "coretypes.h"
 #include "target.h"
@@ -62,6 +65,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "omp-offload.h"  /* For offload_vars.  */
 #include "c-parser.h"
 #include "gcc-urlifier.h"
+#include "c-lsp.h"
 
 #include "tree-pretty-print.h"
 
@@ -14341,4 +14345,88 @@ c_finish_bc_name (location_t loc, tree name, bool is_break)
   return label;
 }
 
+static bool
+make_symbol_for_binding (const c_binding &binding,
+			 lsp::spec::DocumentSymbol &out)
+{
+  if (!binding.id)
+    return false;
+
+  if (!binding.decl)
+    return false;
+
+#if 0
+    p *file_scope->bindings->prev->prev->prev->prev->prev->prev
+
+      (gdb) p *file_scope->bindings->prev->prev->prev->prev->prev->prev
+$11 = {u = {type = <tree 0x0>, label = 0x0}, decl = <record_type 0x7f47101c72a0 foo>, 
+  id = <identifier_node 0x7f47101dec80 foo>, prev = 0x7f47101f9770, shadowed = 0x0, depth = 1, invisible = 0, 
+  nested = 0, inner_comp = 0, in_struct = 0, locus = 1084034}
+
+#endif
+
+  lsp::spec::DocumentSymbol symbol;
+  out.name = IDENTIFIER_POINTER (binding.id);
+
+  out.range = lsp::spec::Range::from_location_t (binding.locus);
+  out.selectionRange = out.range;
+
+  switch (TREE_CODE (binding.decl))
+    {
+    default:
+      gcc_unreachable ();
+
+    case TYPE_DECL:
+      // FIXME: (see e.g. demo.c)
+      return false;
+
+    case FUNCTION_DECL:
+      // Skip builtins
+      if (DECL_BUILT_IN_CLASS (binding.decl) != NOT_BUILT_IN)
+	return false;
+
+      out.kind.m_val = lsp::spec::SymbolKind::Function;
+      // FIXME: add locals?
+      break;
+    case UNION_TYPE:
+    case RECORD_TYPE:
+      out.kind.m_val = lsp::spec::SymbolKind::Struct;
+      for (tree iter = TYPE_FIELDS (binding.decl); iter;
+	   iter = DECL_CHAIN (iter))
+	if (TREE_CODE (iter) == FIELD_DECL)
+	  {
+	    // FIXME: add members
+	  }
+      break;
+    case VAR_DECL:
+      out.kind.m_val = lsp::spec::SymbolKind::Variable;
+      break;
+    }
+
+  return true;
+}
+
+
+lsp::vector<lsp::spec::DocumentSymbol>
+c_lsp_frontend_impl::get_symbols ()
+{
+  lsp::vector<lsp::spec::DocumentSymbol> symbols;
+  // FIXME: TODO
+
+  gcc_assert (file_scope);
+
+  for (auto iter = file_scope->bindings; iter; iter = iter->prev)
+    {
+      lsp::spec::DocumentSymbol symbol;
+      if (make_symbol_for_binding (*iter, symbol))
+	symbols.push_back (std::move (symbol));
+    }
+
+  lsp::vector<lsp::spec::DocumentSymbol> reversed;
+  for (auto iter = std::rbegin (symbols); iter != std::rend (symbols); ++iter)
+    reversed.push_back (std::move (*iter));
+
+  return reversed;
+}
+
 #include "gt-c-c-decl.h"
diff --git a/gcc/c/c-parser.cc b/gcc/c/c-parser.cc
index bc4d8151995..186666981a8 100644
--- a/gcc/c/c-parser.cc
+++ b/gcc/c/c-parser.cc
@@ -37,7 +37,9 @@ along with GCC; see the file COPYING3.  If not see
 
 #include "config.h"
 #define INCLUDE_LIST
+#define INCLUDE_MAP
 #define INCLUDE_STRING
+#define INCLUDE_VECTOR
 #include "system.h"
 #include "coretypes.h"
 #include "target.h"
@@ -80,6 +82,8 @@ along with GCC; see the file COPYING3.  If not see
 #include "gcc-urlifier.h"
 #include "channels.h"
 #include "topics/ast-events.h"
+#include "lsp/gcc-impl/toplev.h"
+#include "c-lsp.h"
 
 
 /* We need to walk over decls with incomplete struct/union/enum types
@@ -2199,6 +2203,12 @@ c_parser_translation_unit (c_parser *parser)
       ana::on_finish_translation_unit (tu);
     }
 #endif
+
+  if (flag_lsp_worker)
+    {
+      c_lsp_frontend_impl impl;
+      gcc_lsp_worker_serve_forever (&impl);
+    }
 }
 
 /* Parse an external declaration (C90 6.7, C99 6.9, C11 6.9).
diff --git a/gcc/common.opt b/gcc/common.opt
index 88b79bbf8f5..d38926cdbef 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -2320,6 +2320,22 @@ flra-remat
 Common Var(flag_lra_remat) Optimization
 Do CFG-sensitive rematerialization in LRA.
 
+flsp-server
+Common RejectNegative Var(flag_lsp_server) Init(0)
+FIXME.
+
+flsp-worker
+Common RejectNegative Var(flag_lsp_worker) Init(0)
+FIXME.
+
+flsp-server-wait-for-debugger
+Common RejectNegative Var(flag_lsp_server_wait_for_debugger) Init(0)
+FIXME.
+
+flsp-worker-wait-for-debugger
+Common RejectNegative Var(flag_lsp_worker_wait_for_debugger) Init(0)
+FIXME.
+
 flto
 Common
 Enable link-time optimization.
diff --git a/gcc/configure.ac b/gcc/configure.ac
index 2868785f201..1cbf4454a42 100644
--- a/gcc/configure.ac
+++ b/gcc/configure.ac
@@ -1397,7 +1397,7 @@ AC_CHECK_HEADERS(ext/hash_map)
 ZW_CREATE_DEPDIR
 AC_CONFIG_COMMANDS([gccdepdir],[
   ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
-  for lang in $subdirs c-family common analyzer custom-sarif-properties diagnostics text-art rtl-ssa sym-exec
+  for lang in $subdirs c-family common analyzer custom-sarif-properties diagnostics lsp text-art rtl-ssa sym-exec
   do
       ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
   done], [subdirs="$subdirs" ac_aux_dir=$ac_aux_dir DEPDIR=$DEPDIR])
diff --git a/gcc/diagnostics/color.cc b/gcc/diagnostics/color.cc
index f639eabbea3..2f885491e11 100644
--- a/gcc/diagnostics/color.cc
+++ b/gcc/diagnostics/color.cc
@@ -104,7 +104,11 @@ static const color_default gcc_color_defaults[] =
   { "valid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
   { "invalid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_RED) },
   { "highlight-a", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
-  { "highlight-b", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_BLUE) }
+  { "highlight-b", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_BLUE) },
+
+  // FIXME: add to docs?
+  { "lsp-server", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_YELLOW) },
+  { "lsp-worker", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_CYAN) }
 };
 
 class diagnostic_color_dict
diff --git a/gcc/diagnostics/digraphs-to-dot-from-cfg.cc b/gcc/diagnostics/digraphs-to-dot-from-cfg.cc
index 428893a8cbe..191f31b3ad8 100644
--- a/gcc/diagnostics/digraphs-to-dot-from-cfg.cc
+++ b/gcc/diagnostics/digraphs-to-dot-from-cfg.cc
@@ -176,16 +176,16 @@ public:
 		  xp.pop_tag ("tr");
 		}
 
-	      if (json::array *arr
+	      if (const json::array *arr
 		  = input_node.get_property (bb_property_names::gimple::phis))
 		print_rows_for_strings (*arr, xp);
 
-	      if (json::array *arr
+	      if (const json::array *arr
 		    = input_node.get_property
 			(bb_property_names::gimple::stmts))
 		print_rows_for_strings (*arr, xp);
 
-	      if (json::array *arr
+	      if (const json::array *arr
 		    = input_node.get_property (bb_property_names::rtl::insns))
 		print_rows_for_strings (*arr, xp);
 
@@ -268,7 +268,7 @@ public:
     }
 
     void
-    print_rows_for_strings (json::array &arr,
+    print_rows_for_strings (const json::array &arr,
 			    xml::printer &xp)
     {
       for (auto iter : arr)
diff --git a/gcc/diagnostics/digraphs.cc b/gcc/diagnostics/digraphs.cc
index 9063f7cc7f3..427706339a0 100644
--- a/gcc/diagnostics/digraphs.cc
+++ b/gcc/diagnostics/digraphs.cc
@@ -50,7 +50,7 @@ digraph_object::get_property (const json::string_property &property) const
   if (!m_property_bag)
     return nullptr;
   if (json::value *jv = m_property_bag->get (property.m_key.get ()))
-    if (json::string *jstr = jv->dyn_cast_string ())
+    if (const json::string *jstr = jv->dyn_cast_string ())
       return jstr->get_string ();
   return nullptr;
 }
@@ -72,7 +72,7 @@ digraph_object::maybe_get_property (const json::integer_property &property,
   if (!m_property_bag)
     return false;
   if (json::value *jv = m_property_bag->get (property.m_key.get ()))
-    if (json::integer_number *jnum = jv->dyn_cast_integer_number ())
+    if (const json::integer_number *jnum = jv->dyn_cast_integer_number ())
       {
 	out_value = jnum->get ();
 	return true;
@@ -116,12 +116,12 @@ get_property_as_tristate (const json::bool_property &property) const
 }
 
 /* Array-of-string properties.  */
-json::array *
+const  json::array *
 digraph_object::get_property (const json::array_of_string_property &property) const
 {
   if (m_property_bag)
     if (json::value *jv = m_property_bag->get (property.m_key.get ()))
-      if (json::array *arr = jv->dyn_cast_array ())
+      if (const json::array *arr = jv->dyn_cast_array ())
 	return arr;
   return nullptr;
 }
diff --git a/gcc/diagnostics/digraphs.h b/gcc/diagnostics/digraphs.h
index 682d980b6ab..e8c4253bb3e 100644
--- a/gcc/diagnostics/digraphs.h
+++ b/gcc/diagnostics/digraphs.h
@@ -71,7 +71,7 @@ public:
   void set_property (const json::bool_property &property, bool value);
 
   /* Array-of-string properties.  */
-  json::array *
+  const json::array *
   get_property (const json::array_of_string_property &property) const;
 
   /* enum properties.  */
diff --git a/gcc/json.cc b/gcc/json.cc
index bff9afffb89..a695d4f3c94 100644
--- a/gcc/json.cc
+++ b/gcc/json.cc
@@ -417,6 +417,17 @@ object::get (const char *key) const
     return NULL;
 }
 
+// FIXME
+
+std::unique_ptr<value>
+object::maybe_take (const char *key)
+{
+  value *val = get (key);
+  if (val)
+    m_map.remove (key);
+  return std::unique_ptr<value> (val);
+}
+
 /* Set value of KEY within this object to a JSON
    string value based on UTF8_VALUE.
    Return a borrowed ptr to the new json::string.  */
diff --git a/gcc/json.h b/gcc/json.h
index 3d79e2a023c..54bc00e964e 100644
--- a/gcc/json.h
+++ b/gcc/json.h
@@ -168,10 +168,11 @@ class value
   void dump (FILE *, bool formatted) const;
   void DEBUG_FUNCTION dump () const;
 
+  virtual const object *dyn_cast_object () const { return nullptr; }
   virtual object *dyn_cast_object () { return nullptr; }
-  virtual array *dyn_cast_array () { return nullptr; }
-  virtual integer_number *dyn_cast_integer_number () { return nullptr; }
-  virtual string *dyn_cast_string () { return nullptr; }
+  virtual const array *dyn_cast_array () const { return nullptr; }
+  virtual const integer_number *dyn_cast_integer_number () const { return nullptr; }
+  virtual const string *dyn_cast_string () const { return nullptr; }
 
   static int compare (const json::value &val_a, const json::value &val_b);
 
@@ -199,6 +200,7 @@ class object : public value
   void print (pretty_printer *pp, bool formatted) const final override;
   std::unique_ptr<value> clone () const final override;
 
+  const object *dyn_cast_object () const final override { return this; }
   object *dyn_cast_object () final override { return this; }
 
   bool is_empty () const { return m_map.is_empty (); }
@@ -222,6 +224,8 @@ class object : public value
   value *get (const char *key) const;
   const map_t &get_map () const { return m_map; }
 
+  std::unique_ptr<value> maybe_take (const char *key);
+
   const json::string *set_string (const char *key, const char *utf8_value);
   const json::integer_number *set_integer (const char *key, long v);
   void set_float (const char *key, double v);
@@ -267,7 +271,7 @@ class array : public value
   void print (pretty_printer *pp, bool formatted) const final override;
   std::unique_ptr<value> clone () const final override;
 
-  array *dyn_cast_array () final override { return this; }
+  const array *dyn_cast_array () const final override { return this; }
 
   void append (value *v);
   const json::string *append_string (const char *utf8_value);
@@ -328,7 +332,7 @@ class integer_number : public value
   void print (pretty_printer *pp, bool formatted) const final override;
   std::unique_ptr<value> clone () const final override;
 
-  integer_number *dyn_cast_integer_number () final override { return this; }
+  const integer_number *dyn_cast_integer_number () const final override { return this; }
 
   long get () const { return m_value; }
 
@@ -349,7 +353,7 @@ class string : public value
   enum kind get_kind () const final override { return JSON_STRING; }
   void print (pretty_printer *pp, bool formatted) const final override;
   std::unique_ptr<value> clone () const final override;
-  string *dyn_cast_string () final override { return this; }
+  const string *dyn_cast_string () const final override { return this; }
 
   const char *get_string () const { return m_utf8; }
   size_t get_length () const { return m_len; }
@@ -385,7 +389,7 @@ object::maybe_get_enum (const enum_property<EnumType> &property,
 			EnumType &out) const
 {
   if (value *jv = get (property.m_key.get ()))
-    if (string *jstr = jv->dyn_cast_string ())
+    if (const string *jstr = jv->dyn_cast_string ())
       {
 	if (enum_traits<EnumType>::maybe_get_value_from_string
 	    (jstr->get_string (), out))
diff --git a/gcc/lsp/README.txt b/gcc/lsp/README.txt
new file mode 100644
index 00000000000..05531eeb0b2
--- /dev/null
+++ b/gcc/lsp/README.txt
@@ -0,0 +1,9 @@
+Overiew of Layers
+=================
+
+ipc: filedescriptors and poll
+octets:
+mini-http:
+json-messaging:
+json-rpc:
+lsp:
diff --git a/gcc/lsp/frontend-interface.h b/gcc/lsp/frontend-interface.h
new file mode 100644
index 00000000000..94b95c4167c
--- /dev/null
+++ b/gcc/lsp/frontend-interface.h
@@ -0,0 +1,47 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_FRONTEND_INTERFACE
+#define GCC_LSP_FRONTEND_INTERFACE
+
+#include "lsp/lsp-spec.h"
+
+namespace lsp {
+
+/* FIXME.  */
+
+class frontend_server_interface
+{
+public:
+private:
+};
+
+class frontend_worker_interface
+{
+public:
+  virtual lsp::vector<lsp::spec::DocumentSymbol>
+  get_symbols () = 0;
+
+private:
+};
+
+} // namespace lsp
+
+#endif  /* GCC_LSP_FRONTEND_INTERFACE  */
diff --git a/gcc/lsp/gcc-impl/common.cc b/gcc/lsp/gcc-impl/common.cc
new file mode 100644
index 00000000000..bebb3a23801
--- /dev/null
+++ b/gcc/lsp/gcc-impl/common.cc
@@ -0,0 +1,45 @@
+/* Implementation details shared by both server and workers.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "lsp/mini-http.h"
+#include "lsp/json-rpc.h"
+#include "lsp/gcc-impl/common.h"
+#include "lsp/octets.h"
+#include "lsp/subprocess.h"
+#include "tree.h"
+#include "topics/ast-events-recorder.h"
+#include "context.h"
+#include "channels.h"
+#include "diagnostic.h"
+#include "diagnostics/color.h"
+
+namespace lsp {
+namespace gcc_impl {
+
+} // namespace lsp::gcc_impl
+} // namespace lsp {
diff --git a/gcc/lsp/gcc-impl/common.h b/gcc/lsp/gcc-impl/common.h
new file mode 100644
index 00000000000..2f90195134a
--- /dev/null
+++ b/gcc/lsp/gcc-impl/common.h
@@ -0,0 +1,37 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+// FIXME: rename
+
+#ifndef GCC_LSP_GCC_IMPL_H
+#define GCC_LSP_GCC_IMPL_H
+
+namespace lsp {
+namespace gcc_impl {
+
+const bool log_lsp = true;
+const bool log_json_rpc = true;
+const bool log_http = true;
+const bool log_octets = true;
+
+} // namespace lsp::gcc_impl
+} // namespace lsp 
+
+#endif  /* GCC_LSP_GCC_IMPL_H  */
diff --git a/gcc/lsp/gcc-impl/server.cc b/gcc/lsp/gcc-impl/server.cc
new file mode 100644
index 00000000000..77511e200d0
--- /dev/null
+++ b/gcc/lsp/gcc-impl/server.cc
@@ -0,0 +1,687 @@
+/* Language Server Protocol implementation.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "lsp/gcc-impl/server.h"
+#include "pretty-print.h"
+#include "options.h"
+#if 0
+#include "lsp/octets.h"
+#include "tree.h"
+#include "topics/ast-events-recorder.h"
+#include "context.h"
+#include "channels.h"
+#include "diagnostic.h"
+#include "diagnostics/color.h"
+#endif
+
+#include "tree.h"
+#include "langhooks.h"
+#include "version.h"
+
+// FIXME
+
+// class json_from_client : public json_rpc::sink
+
+void
+lsp::gcc_impl::server::json_from_client::
+consume_json_rpc_request (json_rpc::request &&req)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  m_mediator.consume_client_request (std::move (req));
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+consume_json_rpc_notification (json_rpc::notification &&n)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  m_mediator.consume_client_notification (std::move (n));
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+consume_json_rpc_response (json_rpc::response &&resp)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+consume_json_rpc_error (json_rpc::error &&err)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+on_malformed_json (json::error &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+on_invalid_json_rpc (json_rpc::error &&err)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_client::
+on_finish ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  // FIXME:
+  //gcc_unreachable ();
+}
+
+// class json_from_worker : public json_rpc::sink
+
+void
+lsp::gcc_impl::server::json_from_worker::
+consume_json_rpc_request (json_rpc::request &&req)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+consume_json_rpc_notification (json_rpc::notification &&n)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  // FIXME: for now, just pass notifications through to client:
+  m_wsp.get_mediator ().send_notification_to_client (std::move (n));
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+consume_json_rpc_response (json_rpc::response &&resp)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  // FIXME: for now, just pass responses through to client:
+  m_wsp.get_mediator ().send_response_to_client (std::move (resp));
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+consume_json_rpc_error (json_rpc::error &&err)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+on_malformed_json (json::error &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+on_invalid_json_rpc (json_rpc::error &&err)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::server::json_from_worker::
+on_finish ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  /* FIXME: do we need to do anything here?
+     If we ignore it, we get into a tight loop of polling
+     and getting POLLHUP.  */
+#if 0
+  m_wsp.get_mediator ().on_worker_hangup (*this)
+#endif
+
+  // FIXME:
+  //gcc_unreachable ();
+}
+
+// class worker_subprocess : public worker_interface_for_server
+
+lsp::gcc_impl::server::worker_subprocess::
+worker_subprocess (mediator &mediator_,
+		   subprocess &&subprocess_)
+: m_mediator (mediator_),
+  m_subprocess (std::move (subprocess_)),
+  m_json_from_worker_sink (*this)
+{
+  m_subprocess.get_receiver_from_stdout ()->set_octet_sink
+    (&m_decode_octets_from_worker);
+  m_decode_octets_from_worker.set_json_rpc_sink (&m_json_from_worker_sink);
+  m_json_from_worker_sink.set_logger (mediator_.get_logger ());
+  m_decode_octets_from_worker.set_logger (mediator_.get_logger ());
+}
+
+void
+lsp::gcc_impl::server::worker_subprocess::
+send_request (json_rpc::request req)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::request::to_json (std::move (req)),
+     m_subprocess.m_sender_to_stdin);
+}
+
+void
+lsp::gcc_impl::server::worker_subprocess::
+send_notification (json_rpc::notification n)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::notification::to_json (std::move (n)),
+     m_subprocess.m_sender_to_stdin);
+}
+
+// class document_version : public lsp::log_user
+
+lsp::gcc_impl::server::document_version::
+document_version (mediator &mediator_,
+		  open_document &open_doc,
+		  lsp::spec::integer version,
+		  lsp::spec::string text)
+: m_mediator (mediator_),
+  m_open_doc (open_doc),
+  m_version (version),
+  m_text (text)
+{
+  start_worker_subprocess ();
+}
+
+void
+lsp::gcc_impl::server::document_version::
+start_worker_subprocess ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  std::list<std::string> args;
+
+  // FIXME:
+  args.push_back ("/proc/self/exe"); // FIXME: Linux-specific ?
+  args.push_back ("-quiet");
+  //args.push_back ("/home/david/nomad-coding/gcc-newgit-no-function-points/src/gcc/testsuite/gcc.dg/lsp/test.c");
+  args.push_back ("-Wall"); // FIXME
+  args.push_back ("-flsp-worker");
+  if (flag_lsp_worker_wait_for_debugger)
+    args.push_back ("-flsp-worker-wait-for-debugger");
+
+  auto launch_result = subprocess::launch (std::move (args), get_logger ());
+  launch_result.set_debug_name
+    (std::string ("worker for \"")
+     + m_open_doc.get_uri ().m_val
+     + "\" version "
+     + std::to_string (m_version.m_val));
+  m_worker
+    = std::make_unique<worker_subprocess> (m_mediator,
+					   std::move (launch_result));
+}
+
+void
+lsp::gcc_impl::server::document_version::
+add_worker_to_event_poll (ipc::event_poll &evp)
+{
+  if (m_worker)
+    m_worker->add_to_event_poll (evp);
+}
+
+// class open_document : public lsp::log_user
+
+lsp::gcc_impl::server::open_document::
+open_document (mediator &mediator_,
+	       const lsp::spec::DidOpenTextDocumentParams &p)
+: m_mediator (mediator_),
+  m_uri (p.textDocument.uri),
+  m_languageId (p.textDocument.languageId)
+{
+  auto doc_version
+    = std::make_unique<document_version> (m_mediator,
+					  *this,
+					  p.textDocument.version,
+					  p.textDocument.text);
+  m_versions.insert ( {p.textDocument.version, std::move (doc_version)} );
+}
+
+void
+lsp::gcc_impl::server::open_document::
+add_workers_to_event_poll (ipc::event_poll &evp)
+{
+  for (auto &iter : m_versions)
+    iter.second->add_worker_to_event_poll (evp);
+}
+
+lsp::gcc_impl::server::document_version *
+lsp::gcc_impl::server::open_document::
+get_document_version (lsp::spec::integer version) const
+{
+  auto iter = m_versions.find (version);
+  if (iter == m_versions.end ())
+    return nullptr;
+  return iter->second.get ();
+}
+
+lsp::gcc_impl::server::document_version *
+lsp::gcc_impl::server::open_document::
+get_most_recent_version () const
+{
+  if (m_versions.size () < 1)
+    return nullptr;
+  auto iter = --m_versions.end ();
+  return iter->second.get ();
+}
+
+void
+lsp::gcc_impl::server::open_document::
+on_didChange (const spec::DidChangeTextDocumentParams &p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  lsp::spec::string new_text;
+  if (p.contentChanges.size () == 1)
+    new_text = p.contentChanges[0].text;
+  else
+    {
+      gcc_unreachable (); // FIXME: error handling
+    }
+  auto doc_version
+    = std::make_unique<document_version> (m_mediator,
+					  *this,
+					  p.textDocument.version,
+					  new_text);
+
+  // FIXME: for now, kill existing subprocess:
+  m_versions.clear ();
+
+  document_version *borrowed_doc_ver = doc_version.get ();
+
+  m_versions.insert ( {p.textDocument.version, std::move (doc_version)} );
+
+  spec::DidOpenTextDocumentParams open;
+  open.textDocument.uri = m_uri;
+  open.textDocument.languageId = m_languageId;
+  open.textDocument.version = p.textDocument.version;
+  open.textDocument.text = std::move (new_text);
+
+  borrowed_doc_ver->get_worker_subprocess ()->send_notification
+    (json_rpc::notification ("textDocument/didOpen",
+			     open.to_json ()));
+}
+
+// class lsp::gcc_impl::server::mediator : public lsp::log_user
+
+lsp::gcc_impl::server::mediator::mediator (lsp::logger *logger)
+  : m_client_ipc_connection (file_descriptor (STDOUT_FILENO),
+			     file_descriptor (STDIN_FILENO)),
+  m_octets_from_client (),
+  m_json_from_client (*this)
+#if 0
+  m_json_rpc_to_lsp_server_for_client (),
+  m_impl_server_for_client (*this)
+#endif
+{
+  LSP_LOG_SCOPE (logger);
+  set_logger (logger);
+
+  m_client_ipc_connection.set_logger (logger);
+
+  m_client_ipc_connection.m_outgoing_sender.set_debug_name
+    ("stdout from server for client");
+
+  m_client_ipc_connection.m_incoming_receiver.set_debug_name
+    ("stdin for server from client");
+  m_client_ipc_connection.m_incoming_receiver.set_octet_sink
+    (&m_octets_from_client);
+
+  m_octets_from_client.set_json_rpc_sink (&m_json_from_client);
+  m_octets_from_client.set_logger (logger);
+
+  m_json_from_client.set_logger (logger);
+
+
+#if 0
+  m_plumbing.connect (&m_json_rpc_to_lsp_server_for_client,
+		      logger);
+  m_json_rpc_to_lsp_server_for_client.set_logger (logger);
+  m_json_rpc_to_lsp_server_for_client.set_lsp_server
+    (&m_impl_server_for_client);
+  m_impl_server_for_client.set_logger (logger);
+#endif
+}
+
+void
+lsp::gcc_impl::server::mediator::handle_events_forever ()
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+  while (1)
+    {
+      ipc::event_poll evp;
+      evp.set_logger (logger);
+
+      evp.add_endpoint (&m_client_ipc_connection.m_incoming_receiver);
+      evp.add_endpoint (&m_client_ipc_connection.m_outgoing_sender);
+      for (auto &iter : m_open_documents)
+	iter.second->add_workers_to_event_poll (evp);
+
+      evp.handle_pending_events (true);
+    }
+}
+
+void
+lsp::gcc_impl::server::mediator::
+consume_client_request (json_rpc::request &&req)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+#if 0
+  if (!req.get_params ())
+    {
+      return json_rpc::response::failure (req.take_id (),
+					  invalid_params ("missing 'params'"));
+    }
+#endif
+  const char *method = req.get_method ();
+  if (logger)
+    logger->log ("call: \"%s\"", method);
+  if (0 == strcmp (method, "initialize"))
+    {
+      auto unmarshal_result
+	= spec::InitializeParams::from_json (req.get_id (),
+					     *req.get_params ());
+      if (unmarshal_result.get_error ())
+	{
+	  gcc_unreachable (); // FIXME
+#if 0
+	  send_response_to_client
+	    (json_rpc::response::invalid_params (req,
+						 std::move (unmarshal_result)));
+#endif
+	  return;
+	}
+
+      on_client_request_initialize (req.take_id (),
+				    unmarshal_result.take_value ());
+      return;
+    }
+  else if (0 == strcmp (method, "shutdown"))
+    {
+      gcc_unreachable ();
+      // return dispatch_call_shutdown (std::move (req));
+    }
+  else if (0 == strcmp (method, "textDocument/documentSymbol"))
+    {
+      auto unmarshal_result
+	= spec::DocumentSymbolParams::from_json (req.get_id (),
+						 *req.get_params ());
+      if (unmarshal_result.get_error ())
+	{
+	  gcc_unreachable (); // FIXME
+#if 0
+	  send_response_to_client
+	    (json_rpc::response::invalid_params (req,
+						 std::move (unmarshal_result)));
+#endif
+	  return;
+	}
+
+      on_client_request_textDocument_documentSymbol
+	(req.take_id (),
+	 unmarshal_result.take_value ());
+      /* We will expect a response from the worker, and forward
+	 that to the client.  */
+      return;
+    }
+#if 0
+  else if (0 == strcmp (method, "textDocument/definition"))
+    return dispatch_textDocument_definition (std::move (req));
+#endif
+
+  send_response_to_client
+    (json_rpc::response::method_not_found (std::move (req)));
+}
+
+void
+lsp::gcc_impl::server::mediator::
+consume_client_notification (json_rpc::notification &&notification)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+    if (!notification.get_params ())
+    {
+      gcc_unreachable (); // FIXME
+      return;
+    }
+
+  const char *method = notification.get_method ();
+
+  if (0 == strcmp (method, "initialized"))
+    {
+      /* no-op.  */
+      return;
+    }
+  else if (0 == strcmp (method, "textDocument/didOpen"))
+    {
+      auto unmarshal_result
+	= spec::DidOpenTextDocumentParams::from_json
+	    (nullptr,
+	     *notification.get_params ());
+      if (unmarshal_result.get_error ())
+	gcc_unreachable (); // FIXME
+
+      on_client_notification_textDocument_didOpen
+	(unmarshal_result.take_value ());
+      return;
+    }
+  else if (0 == strcmp (method, "textDocument/didChange"))
+    {
+      auto unmarshal_result
+	= spec::DidChangeTextDocumentParams::from_json
+	    (nullptr,
+	     *notification.get_params ());
+      if (unmarshal_result.get_error ())
+	gcc_unreachable (); // FIXME
+
+      on_client_notification_textDocument_didChange
+	(unmarshal_result.take_value ());
+      return;
+    }
+  else if (0 == strcmp (method, "textDocument/didClose"))
+    {
+      auto unmarshal_result
+	= spec::DidCloseTextDocumentParams::from_json
+	    (nullptr,
+	     *notification.get_params ());
+      if (unmarshal_result.get_error ())
+	gcc_unreachable (); // FIXME
+
+      on_client_notification_textDocument_didClose
+	(unmarshal_result.take_value ());
+      return;
+    }
+
+  if (logger)
+    logger->log ("unknown notification: \"%s\"", method);
+}
+
+// Handling of requests from client
+
+void
+lsp::gcc_impl::server::mediator::
+on_client_request_initialize (std::unique_ptr<json::value> req_id,
+			      spec::InitializeParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  record_client_init_params (std::move (p));
+
+  lsp::spec::InitializeResult result;
+  result.capabilities = get_ServerCapabilities ();
+  result.serverInfo.set_optional_value (get_serverInfo ());
+
+  send_response_to_client
+    (json_rpc::response::success (std::move (req_id), result.to_json ()));
+}
+
+void
+lsp::gcc_impl::server::mediator::
+on_client_request_textDocument_documentSymbol (std::unique_ptr<json::value> req_id,
+					       spec::DocumentSymbolParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  auto iter = m_open_documents.find (p.textDocument.uri);
+  gcc_assert (iter != m_open_documents.end ());
+
+  auto doc_ver = iter->second->get_most_recent_version ();
+  gcc_assert (doc_ver);
+  doc_ver->get_worker_subprocess ()->send_request
+    (json_rpc::request ("textDocument/documentSymbol",
+			p.to_json (),
+			std::move (req_id)));
+}
+
+// Handling of notifications from client
+
+void
+lsp::gcc_impl::server::mediator::
+on_client_notification_textDocument_didOpen (spec::DidOpenTextDocumentParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  auto doc = std::make_unique<open_document> (*this, p);
+  open_document *new_doc = doc.get ();
+  m_open_documents.insert ({p.textDocument.uri, std::move (doc)});
+
+  document_version *doc_ver
+    = new_doc->get_document_version (p.textDocument.version);
+  gcc_assert (doc_ver);
+
+  doc_ver->get_worker_subprocess ()->send_notification
+    (json_rpc::notification ("textDocument/didOpen", p.to_json ()));
+}
+
+void
+lsp::gcc_impl::server::mediator::
+on_client_notification_textDocument_didChange (spec::DidChangeTextDocumentParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  open_document *open_doc
+    = get_open_document_for_uri (p.textDocument.uri);
+  if (!open_doc)
+    gcc_unreachable (); // FIXME: error handling
+
+  open_doc->on_didChange (p);
+}
+
+void
+lsp::gcc_impl::server::mediator::
+on_client_notification_textDocument_didClose (spec::DidCloseTextDocumentParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  auto iter = m_open_documents.find (p.textDocument.uri);
+  gcc_assert (iter != m_open_documents.end ());
+  m_open_documents.erase (iter);
+}
+
+lsp::gcc_impl::server::open_document *
+lsp::gcc_impl::server::mediator::
+get_open_document_for_uri (const lsp::spec::DocumentUri &uri) const
+{
+  auto iter = m_open_documents.find (uri);
+  if (iter == m_open_documents.end ())
+    return nullptr;
+  return iter->second.get ();
+}
+
+void
+lsp::gcc_impl::server::mediator::
+send_response_to_client (json_rpc::response resp)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::response::to_json (std::move (resp)),
+     get_ipc_sender_for_client ());
+}
+
+void
+lsp::gcc_impl::server::mediator::
+send_notification_to_client (json_rpc::notification n)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::notification::to_json (std::move (n)),
+     get_ipc_sender_for_client ());
+}
+
+void
+lsp::gcc_impl::server::mediator::
+record_client_init_params (lsp::spec::InitializeParams &&p)
+{
+  m_client_init_params
+    = std::make_unique<lsp::spec::InitializeParams> (std::move (p));
+}
+
+lsp::spec::ServerCapabilities
+lsp::gcc_impl::server::mediator::get_ServerCapabilities ()
+{
+  lsp::spec::ServerCapabilities result;
+#if 1
+  result.documentSymbolProvider.set_optional_value
+    (lsp::spec::boolean (true)); // FIXME
+#endif
+  // FIXME
+  return result;
+}
+
+lsp::spec::InitializeResult::serverInfo_t
+lsp::gcc_impl::server::mediator::get_serverInfo ()
+{
+  spec::InitializeResult::serverInfo_t result;
+  result.name = lang_hooks.name;
+  result.version.set_optional_value (version_string);
+  return result;
+}
diff --git a/gcc/lsp/gcc-impl/server.h b/gcc/lsp/gcc-impl/server.h
new file mode 100644
index 00000000000..ff919797acf
--- /dev/null
+++ b/gcc/lsp/gcc-impl/server.h
@@ -0,0 +1,310 @@
+/* FIXME.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+// FIXME: update/rename
+#ifndef GCC_LSP_GCC_LSP_SERVER_H
+#define GCC_LSP_GCC_LSP_SERVER_H
+
+#include "lsp/gcc-impl/common.h"
+#include "lsp/octets-to-json-rpc.h"
+#include "lsp/subprocess.h"
+#include "lsp/lsp-spec.h"
+
+namespace lsp {
+namespace gcc_impl {
+namespace server {
+
+class mediator;
+class worker_subprocess;
+
+// FIXME:
+
+class json_from_worker : public json_rpc::sink
+{
+public:
+  json_from_worker (worker_subprocess &wsp)
+  : m_wsp (wsp)
+  {
+    //m_json_rpc_to_lsp_worker.set_lsp_worker (*wsp);
+  }
+
+  void
+  consume_json_rpc_request (json_rpc::request &&) final override;
+
+  void
+  consume_json_rpc_notification (json_rpc::notification &&) final override;
+
+  void
+  consume_json_rpc_response (json_rpc::response &&) final override;
+
+  void
+  consume_json_rpc_error (json_rpc::error &&) final override;
+
+  void
+  on_malformed_json (json::error &&) final override;
+
+  void
+  on_invalid_json_rpc (json_rpc::error &&) final override;
+
+  void
+  on_finish () final override;
+
+private:
+  worker_subprocess &m_wsp;
+};
+
+// FIXME
+
+class worker_subprocess : public lsp::log_user
+{
+public:
+  worker_subprocess (mediator &mediator_,
+		     subprocess &&subprocess_);
+
+  mediator &get_mediator () const { return m_mediator; }
+
+  void add_to_event_poll (ipc::event_poll &evp)
+  {
+    m_subprocess.add_to_event_poll (evp);
+  }
+
+  void send_request (json_rpc::request req);
+  void send_notification (json_rpc::notification n);
+
+private:
+  mediator &m_mediator;
+  subprocess m_subprocess;
+  octets_to_json_rpc m_decode_octets_from_worker;
+  json_from_worker m_json_from_worker_sink;
+};
+
+class open_document;
+  
+/* A particular version of an open text document.
+   Potentially has a subprocess.  */
+  
+class document_version : public lsp::log_user
+{
+public:
+  document_version (mediator &mediator_,
+		    open_document &open_doc,
+		    lsp::spec::integer version,
+		    lsp::spec::string text);
+
+  void start_worker_subprocess ();
+  worker_subprocess *get_worker_subprocess () const { return m_worker.get (); }
+
+  void add_worker_to_event_poll (ipc::event_poll &evp);
+
+private:
+  mediator &m_mediator;
+  open_document &m_open_doc;
+  lsp::spec::integer m_version;
+  lsp::spec::string m_text;
+  std::unique_ptr<worker_subprocess> m_worker;
+};
+
+/* The server's knowledge about an open text document
+   (identified via DocumentUri).
+   Tracks the various versions of the document.  */
+   
+class open_document : public lsp::log_user
+{
+public:
+  open_document (mediator &mediator_,
+		 const lsp::spec::DidOpenTextDocumentParams &p);
+
+  void add_workers_to_event_poll (ipc::event_poll &evp);
+
+  mediator &get_mediator () const { return m_mediator; }
+
+  const lsp::spec::DocumentUri &get_uri () const { return m_uri; }
+
+  document_version *
+  get_document_version (lsp::spec::integer) const;
+
+  document_version *
+  get_most_recent_version () const;
+
+  void
+  on_didChange (const spec::DidChangeTextDocumentParams &p);
+
+private:
+  mediator &m_mediator;
+  lsp::spec::DocumentUri m_uri;
+  lsp::spec::string m_languageId;
+  std::map<lsp::spec::integer, std::unique_ptr<document_version>> m_versions;
+};
+
+class json_from_client : public json_rpc::sink
+{
+public:
+  json_from_client (mediator &mediator_)
+  : m_mediator (mediator_) {}
+
+  void
+  consume_json_rpc_request (json_rpc::request &&) final override;
+
+  void
+  consume_json_rpc_notification (json_rpc::notification &&) final override;
+
+  void
+  consume_json_rpc_response (json_rpc::response &&) final override;
+
+  void
+  consume_json_rpc_error (json_rpc::error &&) final override;
+
+  void
+  on_malformed_json (json::error &&) final override;
+
+  void
+  on_invalid_json_rpc (json_rpc::error &&) final override;
+
+  void
+  on_finish () final override;
+
+private:
+  mediator &m_mediator;
+};
+
+#if 0
+class impl_server_for_client : public lsp::noop_server_impl_for_client
+{
+public:
+  impl_server_for_client (mediator &mediator_) : m_mediator (mediator_) {}
+
+  lsp::result<lsp::spec::InitializeResult>
+  call_initialize (lsp::spec::InitializeParams &&) override;
+
+  void
+  notify_initialized (const lsp::spec::InitializedParams &) override;
+
+  void
+  notify_textDocument_didOpen (const lsp::spec::DidOpenTextDocumentParams &p) override;
+
+  void
+  notify_textDocument_didChange (const lsp::spec::DidChangeTextDocumentParams &p) override;
+
+  void
+  notify_textDocument_didClose (const spec::DidCloseTextDocumentParams &p) override;
+
+#if 0
+  /* Implementation of "textDocument/definition":
+     find the definition of the symbol at a given source location.  */
+  lsp::result<std::vector<lsp::spec::Location>>
+  on_textDocument_definition (const lsp::spec::TextDocumentPositionParams &p) override;
+#endif
+
+private:
+  mediator &m_mediator;
+};
+#endif
+
+/* A central hub to simplify interactions between objects within the server:
+   objects couple with this, rather than with each other.  */
+
+class mediator : public lsp::log_user
+{
+public:
+  mediator (lsp::logger *logger);
+
+  void
+  handle_events_forever ();
+
+  void consume_client_request (json_rpc::request &&req);
+  void consume_client_notification (json_rpc::notification &&n);
+
+  void
+  send_response_to_client (json_rpc::response resp);
+
+  void
+  send_notification_to_client (json_rpc::notification n);
+
+  // Handling of requests from client
+  
+  void
+  on_client_request_initialize (std::unique_ptr<json::value> req_id,
+				spec::InitializeParams &&p);
+
+  void
+  on_client_request_textDocument_documentSymbol (std::unique_ptr<json::value> req_id,
+						 spec::DocumentSymbolParams &&p);
+
+  // Handling of notifications from client
+
+  void
+  on_client_notification_textDocument_didOpen (spec::DidOpenTextDocumentParams &&p);
+
+  void
+  on_client_notification_textDocument_didChange (spec::DidChangeTextDocumentParams &&p);
+
+  void
+  on_client_notification_textDocument_didClose (spec::DidCloseTextDocumentParams &&p);
+
+
+#if 0
+  void
+  add_open_document (const lsp::spec::DidOpenTextDocumentParams &p);
+
+  void
+  close_document (const lsp::spec::DidCloseTextDocumentParams &p);
+#endif
+
+  open_document *
+  get_open_document_for_uri (const lsp::spec::DocumentUri &uri) const;
+
+#if 0
+  worker_subprocess *
+  start_worker_for_uri (lsp::spec::DocumentUri uri);
+#endif
+
+  ipc::sender &
+  get_ipc_sender_for_client ()
+  {
+    return m_client_ipc_connection.m_outgoing_sender;
+  }
+
+  void
+  record_client_init_params (lsp::spec::InitializeParams &&p);
+
+  spec::ServerCapabilities get_ServerCapabilities ();
+  spec::InitializeResult::serverInfo_t get_serverInfo ();
+
+private:
+  ipc::connection m_client_ipc_connection;
+  octets_to_json_rpc m_octets_from_client;
+  json_from_client m_json_from_client;
+#if 0
+  json_rpc_to_lsp_server_for_client m_json_rpc_to_lsp_server_for_client;
+  impl_server_for_client m_impl_server_for_client;
+#endif
+
+  std::unique_ptr<lsp::spec::InitializeParams> m_client_init_params;
+  
+  using document_map_t = std::map<lsp::spec::DocumentUri,
+				  std::unique_ptr<open_document>>;
+  document_map_t m_open_documents;
+};
+
+} // namespace lsp::gcc_impl::server
+} // namespace lsp::gcc_impl
+} // namespace lsp
+
+#endif  /* GCC_LSP_GCC_LSP_SERVER_H  */
diff --git a/gcc/lsp/gcc-impl/toplev.cc b/gcc/lsp/gcc-impl/toplev.cc
new file mode 100644
index 00000000000..87b413e064f
--- /dev/null
+++ b/gcc/lsp/gcc-impl/toplev.cc
@@ -0,0 +1,118 @@
+/* Entrypoints to LSP implementation for the rest of GCC.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "lsp/gcc-impl/toplev.h"
+#include "lsp/gcc-impl/server.h"
+#include "lsp/gcc-impl/worker.h"
+#include "pretty-print.h"
+#include "diagnostic.h"
+#include "diagnostics/color.h"
+#include "options.h"
+
+// FIXME: these should be options:
+const bool log_lsp = true; 
+const bool log_json_rpc = true;
+const bool log_http = true;
+const bool log_octets = true;
+
+static lsp::logger *lsp_logger;
+
+lsp::logger *
+make_logger (std::string prefix, const char *color_name)
+{
+  bool show_color = true; // FIXME: only if at a tty, or respect options
+  const char *start_color
+    = colorize_start (show_color, color_name, strlen (color_name));
+  const char *stop_color = colorize_stop (show_color);
+  
+  return new lsp::logger (stderr, std::move (prefix),
+			  start_color, stop_color);
+}
+
+// FIXME
+
+static void
+do_init (bool is_server, bool wait_for_debugger)
+{
+  pretty_printer pp;
+  pp_printf (&pp, "[%s:%s:%i] ",
+	     progname,
+	     is_server ? "server" : "worker",
+	     (int)getpid ());
+  std::string prefix (pp_formatted_text (&pp));
+
+  lsp_logger = make_logger (std::move (prefix),
+			    is_server ? "lsp-server" : "lsp-worker");
+
+  if (wait_for_debugger)
+    {
+      lsp_logger->log ("waiting for debugger; use");
+      lsp_logger->emit_start_color ();
+      fprintf (stderr, "   $ gdb attach -p %i\n", getpid ());
+      fprintf (stderr, "   (gdb) set wait_for_debugger = 0\n");
+      fprintf (stderr, "   (gdb) continue\n");
+      lsp_logger->emit_stop_color ();
+
+      while (wait_for_debugger)
+	{
+	  // wait until flag is cleared in debugger
+	}
+    }
+}
+
+/* Entrypoint to server.  */
+
+void
+gcc_lsp_server_serve_forever ()
+{
+  do_init (true, flag_lsp_server_wait_for_debugger);
+  lsp::gcc_impl::server::mediator server_mediator (lsp_logger);
+  server_mediator.handle_events_forever ();
+}
+
+/* Entrypoints to worker.  */
+
+static std::unique_ptr<lsp::gcc_impl::worker::mediator> worker_mediator;
+
+/* Initialize worker::mediator and poll until we have a didOpen with the
+   main filename and its content.
+   Return the main filename from the didOpen notification.  */
+   
+const char *
+gcc_lsp_worker_init ()
+{
+  do_init (false, flag_lsp_worker_wait_for_debugger);
+  worker_mediator
+    = std::make_unique<lsp::gcc_impl::worker::mediator> (lsp_logger);
+  return worker_mediator->handle_events_until_didOpen ();
+}
+
+void
+gcc_lsp_worker_serve_forever (lsp::frontend_worker_interface *frontend)
+{
+  worker_mediator->handle_events_after_compile (frontend);
+}
diff --git a/gcc/lsp/gcc-impl/toplev.h b/gcc/lsp/gcc-impl/toplev.h
new file mode 100644
index 00000000000..f00d224a2a4
--- /dev/null
+++ b/gcc/lsp/gcc-impl/toplev.h
@@ -0,0 +1,41 @@
+/* Entrypoints to LSP implementation for the rest of GCC.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+// FIXME: rename
+
+#ifndef GCC_LSP_GCC_LSP_TOPLEV_H
+#define GCC_LSP_GCC_LSP_TOPLEV_H
+
+#include "lsp/frontend-interface.h"
+
+/* Entrypoint to LSP server.  */
+
+extern void
+gcc_lsp_server_serve_forever ();
+
+/* Entrypoints to LSP worker.  */
+
+extern const char *
+gcc_lsp_worker_init ();
+
+extern void
+gcc_lsp_worker_serve_forever (lsp::frontend_worker_interface *frontend);
+
+#endif  /* GCC_LSP_GCC_LSP_TOPLEV_H  */
diff --git a/gcc/lsp/gcc-impl/worker-diagnostics.cc b/gcc/lsp/gcc-impl/worker-diagnostics.cc
new file mode 100644
index 00000000000..d041e087545
--- /dev/null
+++ b/gcc/lsp/gcc-impl/worker-diagnostics.cc
@@ -0,0 +1,416 @@
+/* LSP support for GCC diagnostics.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "lsp/gcc-impl/worker.h"
+#include "lsp/gcc-impl/worker-diagnostics.h"
+#include "lsp/lsp-spec.h"
+//#include "lsp/lsp-client-iface.h"
+//#include "lsp/lsp-gcc-wo-manager.h"
+#include "diagnostic.h"
+#include "diagnostics/sink.h"
+#include "diagnostics/dumping.h"
+#include "uris.h"
+
+using top_level_diagnostic
+  = lsp::gcc_impl::worker::diagnostics::top_level_diagnostic;
+
+static void
+dump_diagnostic (const lsp::spec::Diagnostic &d,
+		 FILE *out, int indent)
+{
+  auto jv = d.to_json ();
+  diagnostics::dumping::emit_indent (out, indent);
+  jv->dump (out, false);
+  fprintf (out, "\n");
+}
+
+// class lsp::gcc_impl::worker::diagnostics::per_sink_buffer
+
+void
+lsp::gcc_impl::worker::diagnostics::
+per_sink_buffer::dump (FILE *out, int indent) const
+{
+  gcc_unreachable (); // FIXME
+}
+
+bool
+lsp::gcc_impl::worker::diagnostics::per_sink_buffer::empty_p () const
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::per_sink_buffer::
+move_to (::diagnostics::per_sink_buffer &dest)
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::per_sink_buffer::clear ()
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::per_sink_buffer::flush ()
+{
+  gcc_unreachable (); // FIXME
+}
+
+// class lsp::gcc_impl::worker::diagnostics::sink
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::dump_kind (FILE *out) const
+{
+  fprintf (out, "lsp::gcc_impl::worker::diagnostics::sink");
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::dump (FILE *out, int indent) const
+{
+  ::diagnostics::sink::dump (out, indent);
+#if 0
+  ::diagnostics::dumping::emit_bool_field
+      (out, indent,
+       "server initialized", m_server.initialized_p ());
+#endif
+  if (m_pending_diagnostics.size () > 0)
+    {
+      ::diagnostics::dumping::emit_heading (out, indent, "pending diagnostics");
+      for (auto &iter_map : m_pending_diagnostics)
+	{
+	  ::diagnostics::dumping::emit_heading (out, indent + 2, "DocumentUri: FIXME");
+	  for (auto &d : *(iter_map.second))
+	    dump_diagnostic (d, out, indent + 4);
+	}
+    }
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+set_main_input_filename (const char *name)
+{
+  gcc_unreachable (); // FIXME
+}
+
+std::unique_ptr<::diagnostics::per_sink_buffer>
+lsp::gcc_impl::worker::diagnostics::sink::make_per_sink_buffer ()
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+set_buffer (::diagnostics::per_sink_buffer *base_buffer)
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::on_begin_group ()
+{
+  // No-op
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::on_end_group ()
+{
+  if (m_cur_top_level_diagnostic)
+    {
+      if (m_mediator.done_compiling_p ())
+	{
+	  gcc_unreachable (); // FIXME
+	}
+      else
+	{
+	  auto uri_iter
+	    = m_pending_diagnostics.find (m_cur_top_level_diagnostic->m_uri);
+	  if (uri_iter == m_pending_diagnostics.end ())
+	    {
+	      auto diag_vec
+		= std::make_unique<std::vector<lsp::spec::Diagnostic>> ();
+	      auto r = m_pending_diagnostics.insert
+		({std::move (m_cur_top_level_diagnostic->m_uri),
+		  std::move (diag_vec)});
+	      uri_iter = r.first;
+	    }
+	  uri_iter->second->push_back
+	    (std::move (m_cur_top_level_diagnostic->m_diagnostic));
+	}
+      m_cur_top_level_diagnostic = nullptr;
+    }
+}
+
+void
+top_level_diagnostic::
+add_nested_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+		       enum ::diagnostics::kind orig_diag_kind,
+		       ::diagnostics::sink &sink)
+{
+  lsp::spec::DiagnosticRelatedInformation relatedInformation;
+
+  // Location information
+  {
+    location_t primary_loc = diagnostic_location (&diagnostic, 0);
+    relatedInformation.location
+      = lsp::spec::Location::from_location_t (primary_loc);
+    // We don't expose secondary locations within nested diagnostics
+  }
+
+  // Get message
+  {
+    pretty_printer *pp = sink.get_printer ();
+    pp_output_formatted_text (pp, sink.get_context ().get_urlifier ());
+    relatedInformation.message.m_val = std::string (pp_formatted_text (pp));
+    pp_clear_output_area (pp);
+  }
+
+  if (m_diagnostic.relatedInformation.get_optional_value ())
+    m_diagnostic.relatedInformation.set_optional_value ({});
+  m_diagnostic.relatedInformation.get_optional_value ()->push_back
+    (std::move (relatedInformation));
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+on_report_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+		      enum ::diagnostics::kind orig_diag_kind)
+{
+#if 0
+  if (diagnostic.m_kind == kind::ice || diagnostic.m_kind == kind::ice_nobt)
+    {
+      gcc_unreachable (); // FIXME
+    }
+#endif
+
+  if (m_cur_top_level_diagnostic)
+    m_cur_top_level_diagnostic->add_nested_diagnostic (diagnostic,
+						       orig_diag_kind,
+						       *this);
+  else
+    m_cur_top_level_diagnostic
+      = std::make_unique<top_level_diagnostic>
+	  (make_top_level_diagnostic (diagnostic, orig_diag_kind));
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+on_diagram (const ::diagnostics::diagram &)
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+after_diagnostic (const ::diagnostics::diagnostic_info &)
+{
+  // gcc_unreachable (); // FIXME
+}
+
+bool
+lsp::gcc_impl::worker::diagnostics::sink::machine_readable_stderr_p () const
+{
+  gcc_unreachable (); // FIXME
+}
+
+bool
+lsp::gcc_impl::worker::diagnostics::sink::follows_reference_printer_p () const
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::update_printer ()
+{
+  m_printer = m_context.clone_printer ();
+
+  /* Don't colorize the text.  */
+  pp_show_color (m_printer.get ()) = false;
+
+  /* No textual URLs.  */
+  m_printer->set_url_format (URL_FORMAT_NONE);
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+report_global_digraph (const lazily_created<::diagnostics::digraphs::digraph> &)
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+report_digraph_for_logical_location (const lazily_created<::diagnostics::digraphs::digraph> &,
+				     ::diagnostics::logical_locations::key )
+{
+  gcc_unreachable (); // FIXME
+}
+
+void
+lsp::gcc_impl::worker::diagnostics::sink::
+flush_pending_diagnostics ()
+{
+  for (auto &iter_map : m_pending_diagnostics)
+    {
+      lsp::spec::PublishDiagnosticsParams params;
+      params.uri = iter_map.first;
+      for (auto &d : *iter_map.second)
+	params.diagnostics.push_back (std::move (d));
+      auto n = json_rpc::notification ("textDocument/publishDiagnostics",
+				       params.to_json ());
+      m_mediator.send_notification_to_server (std::move (n));
+    }
+  m_pending_diagnostics.clear ();
+}
+
+static lsp::spec::DiagnosticSeverity
+get_severity_for_kind (diagnostics::kind kind)
+{
+  lsp::spec::DiagnosticSeverity result;
+  switch (kind)
+    {
+    case diagnostics::kind::debug:
+    case diagnostics::kind::note:
+      result.m_val = lsp::spec::DiagnosticSeverity::Information;
+      break;
+
+    case diagnostics::kind::anachronism:
+    case diagnostics::kind::warning:
+      result.m_val = lsp::spec::DiagnosticSeverity::Warning;
+      break;
+
+    case diagnostics::kind::error:
+    case diagnostics::kind::sorry:
+    case diagnostics::kind::ice:
+    case diagnostics::kind::ice_nobt:
+    case diagnostics::kind::fatal:
+      result.m_val = lsp::spec::DiagnosticSeverity::Error;
+      break;
+
+    default:
+      gcc_unreachable ();
+    }
+  return result;
+}
+
+// FIXME: this was adapted from sarif-sink.cc: sarif_builder::make_location_object
+
+static lsp::vector<lsp::spec::DiagnosticRelatedInformation>
+get_relatedInformation_from_rich_location (const rich_location &rich_loc)
+{
+  lsp::vector<lsp::spec::DiagnosticRelatedInformation> result;
+  for (unsigned int i = 0; i < rich_loc.get_num_locations (); i++)
+    {
+      const location_range *range = rich_loc.get_range (i);
+      bool handled = false;
+      if (const range_label *label = range->m_label)
+	{
+	  label_text text = label->get_text (i);
+	  if (text.get ())
+	    {
+	      /* Create annotations for any labelled ranges.  */
+	      lsp::spec::DiagnosticRelatedInformation relatedInformation;
+
+	      location_t range_loc = rich_loc.get_loc (i);
+	      relatedInformation.location
+		= lsp::spec::Location::from_location_t (range_loc);
+	      relatedInformation.message = lsp::spec::string (text.get ());
+	      result.push_back (std::move (relatedInformation));
+	      handled = true;
+	    }
+	}
+
+      if (!handled)
+	{
+	  lsp::spec::DiagnosticRelatedInformation relatedInformation;
+	  location_t range_loc = rich_loc.get_loc (i);
+	  relatedInformation.location
+	    = lsp::spec::Location::from_location_t (range_loc);
+	  relatedInformation.message = "FIXME";
+	  result.push_back (std::move (relatedInformation));
+	}
+    }
+  return result;
+}
+
+top_level_diagnostic
+lsp::gcc_impl::worker::diagnostics::sink::
+make_top_level_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+			   enum ::diagnostics::kind orig_diag_kind)
+{
+  lsp::spec::Diagnostic lsp_diag;
+  lsp::spec::DocumentUri uri;
+
+  // Location information
+  {
+    location_t primary_loc = diagnostic_location (&diagnostic, 0);
+    lsp_diag.range = lsp::spec::Range::from_location_t (primary_loc);
+
+    // Get URI:
+    expanded_location exp_loc = expand_location (primary_loc);
+    if (exp_loc.file)
+      {
+	uris::file_uri file_uri (exp_loc.file);
+	uri.m_val = file_uri.to_string ();
+      }
+
+    // Get secondary locations as relatedLocations
+    vector<lsp::spec::DiagnosticRelatedInformation> relatedInformation
+      = get_relatedInformation_from_rich_location (*diagnostic.m_richloc);
+    if (relatedInformation.size () > 0)
+      lsp_diag.relatedInformation.set_optional_value
+	(std::move (relatedInformation));
+  }
+
+  lsp_diag.severity.set_optional_value
+    (get_severity_for_kind (diagnostic.m_kind));
+
+  label_text option_text
+    = m_context.make_option_name (diagnostic.m_option_id,
+				  orig_diag_kind, diagnostic.m_kind);
+  if (option_text.get ())
+    lsp_diag.code.set_optional_value (option_text.get ());
+
+  label_text option_url
+    = m_context.make_option_url (diagnostic.m_option_id);
+  if (option_url.get ())
+    lsp_diag.codeDescription.set_optional_value ({option_url.get ()});
+
+  // Get message
+  {
+    pretty_printer *pp = get_printer ();
+    pp_output_formatted_text (pp, m_context.get_urlifier ());
+    lsp_diag.message.m_val = std::string (pp_formatted_text (pp));
+    pp_clear_output_area (pp);
+  }
+
+  return {uri, std::move (lsp_diag)};
+}
+
+// FIXME: selftests
diff --git a/gcc/lsp/gcc-impl/worker-diagnostics.h b/gcc/lsp/gcc-impl/worker-diagnostics.h
new file mode 100644
index 00000000000..df9f42e13ca
--- /dev/null
+++ b/gcc/lsp/gcc-impl/worker-diagnostics.h
@@ -0,0 +1,163 @@
+/* LSP support for GCC diagnostics.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+// FIXME: rename, probably to gcc/lsp/lsp-gcc-worker-impl-diagnostics.h
+#ifndef GCC_LSP_LSP_DIAGNOSTICS_H
+#define GCC_LSP_LSP_DIAGNOSTICS_H
+
+#include "diagnostics/sink.h"
+#include "diagnostics/buffering.h"
+#include "lsp/lsp-spec.h"
+
+namespace lsp {
+namespace gcc_impl {
+namespace worker {
+
+class mediator;
+
+namespace diagnostics {
+
+/* Concrete buffering implementation subclass for LSP diagnostic sink.  */
+
+class per_sink_buffer : public ::diagnostics::per_sink_buffer
+{
+public:
+  void dump (FILE *out, int indent) const final override;
+  bool empty_p () const final override;
+  void move_to (::diagnostics::per_sink_buffer &dest) final override;
+  void clear () final override;
+  void flush () final override;
+
+  void add_diagnostic (lsp::spec::Diagnostic d)
+  {
+    m_diagnostics.push_back (std::move (d));
+  }
+
+private:
+  std::vector<lsp::spec::Diagnostic> m_diagnostics;
+};
+
+struct top_level_diagnostic
+{
+  void
+  add_nested_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+			 enum ::diagnostics::kind orig_diag_kind,
+			 ::diagnostics::sink &sink);
+
+  lsp::spec::DocumentUri m_uri;
+  lsp::spec::Diagnostic m_diagnostic;
+};
+
+class sink : public ::diagnostics::sink
+{
+public:
+  sink (::diagnostics::context &dc,
+	mediator &mediator_)
+  : ::diagnostics::sink (dc),
+    m_mediator (mediator_),
+    m_buffer (nullptr)
+  {}
+
+  ~sink ()
+  {
+#if 0
+    /* Any diagnostics should have been handled by now.
+       If not, then something's gone wrong with diagnostic
+       groupings.  */
+    std::unique_ptr<xml::element> pending_diag
+      = m_builder.take_current_diagnostic ();
+    gcc_assert (!pending_diag);
+#endif
+  }
+
+  void
+  dump_kind (FILE *out) const override;
+
+  void
+  dump (FILE *out, int indent) const final override;
+
+  void
+  set_main_input_filename (const char *name) final override;
+
+  std::unique_ptr<::diagnostics::per_sink_buffer>
+  make_per_sink_buffer () final override;
+
+  void
+  set_buffer (::diagnostics::per_sink_buffer *base_buffer) final override;
+
+  void on_begin_group () final override;
+  void on_end_group () final override;
+
+  void
+  on_report_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+			enum ::diagnostics::kind orig_diag_kind) final override;
+
+  void
+  on_diagram (const ::diagnostics::diagram &) final override;
+
+  void
+  after_diagnostic (const ::diagnostics::diagnostic_info &) final override;
+
+  bool
+  machine_readable_stderr_p () const final override;
+
+  bool
+  follows_reference_printer_p () const final override;
+
+  void
+  update_printer () final override;
+
+  void
+  report_global_digraph (const lazily_created<::diagnostics::digraphs::digraph> &);
+
+  void
+  report_digraph_for_logical_location (const lazily_created<::diagnostics::digraphs::digraph> &,
+				       ::diagnostics::logical_locations::key ) final override;
+
+  // On initialization completed
+  void
+  flush_pending_diagnostics ();
+
+private:
+  top_level_diagnostic
+  make_top_level_diagnostic (const ::diagnostics::diagnostic_info &diagnostic,
+			     enum ::diagnostics::kind orig_diag_kind);
+
+  mediator &m_mediator;
+
+  per_sink_buffer *m_buffer;
+
+  /* The top-level diagnostic in the current diagnostic group, if any.  */
+  std::unique_ptr<top_level_diagnostic> m_cur_top_level_diagnostic;
+
+  /* We can't report diagnostics to the client until the connection is fully
+     initialized.  Until then, buffer them here, per DocumentUri.  */
+  using pending_diagnostics_map_t
+    = std::map<spec::DocumentUri, std::unique_ptr<std::vector<lsp::spec::Diagnostic>>>;
+  pending_diagnostics_map_t m_pending_diagnostics;
+
+};
+
+} // namespace lsp::gcc_impl_worker::diagnostics
+} // namespace lsp::gcc_impl::worker
+} // namespace lsp::gcc_impl
+} // namespace lsp
+
+#endif  /* GCC_LSP_LSP_DIAGNOSTICS_H  */
diff --git a/gcc/lsp/gcc-impl/worker.cc b/gcc/lsp/gcc-impl/worker.cc
new file mode 100644
index 00000000000..def78373cd1
--- /dev/null
+++ b/gcc/lsp/gcc-impl/worker.cc
@@ -0,0 +1,404 @@
+/* Language Server Protocol implementation.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "lsp/mini-http.h"
+#include "lsp/json-rpc.h"
+#include "lsp/gcc-impl/toplev.h"
+#include "lsp/gcc-impl/worker.h"
+#include "lsp/gcc-impl/worker-diagnostics.h"
+#include "lsp/octets.h"
+#include "lsp/subprocess.h"
+#include "tree.h"
+#include "topics/ast-events-recorder.h"
+#include "context.h"
+#include "channels.h"
+#include "uris.h"
+#if 0
+#include "diagnostic.h"
+#include "diagnostics/color.h"
+#endif
+
+#include "../libcpp/internal.h"
+
+// FIXME:
+extern struct cpp_reader* parse_in;
+
+// class lsp::gcc_impl::worker::mediator : public lsp::log_user
+
+lsp::gcc_impl::worker::mediator::mediator (lsp::logger *logger)
+: m_server_ipc_connection (file_descriptor (STDOUT_FILENO),
+			   file_descriptor (STDIN_FILENO)),
+  m_filename_from_didOpen (nullptr),
+  m_lsp_diagnostic_sink (nullptr),
+  m_done_compiling_p (false),
+  m_frontend (nullptr)
+{
+  LSP_LOG_SCOPE (logger);
+  set_logger (logger);
+
+  m_server_ipc_connection.set_logger (logger);
+
+  m_server_ipc_connection.m_outgoing_sender.set_debug_name
+    ("stdout from worker for server");
+  m_server_ipc_connection.m_incoming_receiver.set_debug_name
+    ("stdin for worker to server");
+  m_server_ipc_connection.m_incoming_receiver.set_octet_sink
+    (&m_octets_from_server);
+
+  m_octets_from_server.set_json_rpc_sink (this);
+  m_octets_from_server.set_logger (logger);
+
+  /* Record AST events.  */
+  g->get_channels ().ast_events_channel.add_subscriber (m_recorder);
+
+  /* Listen for diagnostics via a custom diagnostics::sink.  */
+  {
+    auto sink
+      = std::make_unique<lsp::gcc_impl::worker::diagnostics::sink>
+          (*global_dc, *this);
+    m_lsp_diagnostic_sink = sink.get ();
+    sink->update_printer ();
+#if 1
+    global_dc->set_sink (std::move (sink));
+#else
+    global_dc->add_sink (std::move (sink));
+#endif
+  }
+
+}
+
+const char *
+lsp::gcc_impl::worker::mediator::handle_events_until_didOpen ()
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (!m_filename_from_didOpen);
+
+  while (1)
+    {
+      ipc::event_poll evp;
+      evp.set_logger (logger);
+
+      evp.add_endpoint (&m_server_ipc_connection.m_incoming_receiver);
+      evp.add_endpoint (&m_server_ipc_connection.m_outgoing_sender);
+      evp.handle_pending_events (true);
+
+      if (m_filename_from_didOpen)
+	return m_filename_from_didOpen;
+    }  
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+handle_events_after_compile (lsp::frontend_worker_interface *frontend)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (m_filename_from_didOpen);
+
+  m_done_compiling_p = true;
+
+  m_frontend = frontend;
+
+  // FIXME:
+  //dump_ast ();
+
+  // Flush any pending diagnostics
+  gcc_assert (m_lsp_diagnostic_sink);
+  m_lsp_diagnostic_sink->flush_pending_diagnostics ();
+
+  // Handle any pending requests
+  for (auto &&req : m_queued_requests)
+    {
+      if (logger)
+	logger->log ("consuming queued request");
+      consume_json_rpc_request (std::move (req));
+    }
+
+  while (1)
+    {
+      ipc::event_poll evp;
+      evp.set_logger (logger);
+
+      evp.add_endpoint (&m_server_ipc_connection.m_incoming_receiver);
+      evp.add_endpoint (&m_server_ipc_connection.m_outgoing_sender);
+      evp.handle_pending_events (true);
+    }  
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+send_response_to_server (json_rpc::response r)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::response::to_json (std::move (r)),
+     get_ipc_sender_for_server ());
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+send_notification_to_server (json_rpc::notification n)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  json_rpc::send_json_message_to_ipc_sender
+    (json_rpc::notification::to_json (std::move (n)),
+     get_ipc_sender_for_server ());
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+consume_json_rpc_request (json_rpc::request &&req)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+  if (!m_done_compiling_p)
+    {
+      if (logger)
+	logger->log ("queuing request until after compilation");
+      m_queued_requests.push_back (std::move (req));
+      return;
+    }
+
+  gcc_assert (m_frontend);
+    
+#if 0
+  if (!req.get_params ())
+    {
+      return json_rpc::response::failure (req.take_id (),
+					  invalid_params ("missing 'params'"));
+    }
+#endif
+  const char *method = req.get_method ();
+  if (logger)
+    logger->log ("call: \"%s\"", method);
+  if (0 == strcmp (method, "textDocument/documentSymbol"))
+    {
+      auto unmarshal_result
+	= spec::DocumentSymbolParams::from_json (req.get_id (),
+						 *req.get_params ());
+      if (unmarshal_result.get_error ())
+	{
+	  gcc_unreachable (); // FIXME
+#if 0
+	  send_response_to_client
+	    (json_rpc::response::invalid_params (req,
+						 std::move (unmarshal_result)));
+#endif
+	  return;
+	}
+
+      on_request_textDocument_documentSymbol
+	(req.take_id (),
+	 unmarshal_result.take_value ());
+      return;
+    }
+#if 0
+  else if (0 == strcmp (method, "textDocument/definition"))
+    return dispatch_textDocument_definition (std::move (req));
+#endif
+
+  send_response_to_server
+    (json_rpc::response::method_not_found (std::move (req)));
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+consume_json_rpc_notification (json_rpc::notification &&notification)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  if (!notification.get_params ())
+    {
+      gcc_unreachable (); // FIXME
+      return;
+    }
+
+  const char *method = notification.get_method ();
+
+  if (0 == strcmp (method, "textDocument/didOpen"))
+    {
+      auto unmarshal_result
+	= spec::DidOpenTextDocumentParams::from_json (nullptr,
+						      *notification.get_params ());
+      if (unmarshal_result.get_error ())
+	gcc_unreachable (); // FIXME
+
+      on_notification_textDocument_didOpen (unmarshal_result.take_value ());
+      return;
+    }
+
+  if (get_logger ())
+    get_logger ()->log ("unknown notification: \"%s\"", method);
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+consume_json_rpc_response (json_rpc::response &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+consume_json_rpc_error (json_rpc::error &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+on_malformed_json (json::error &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+on_invalid_json_rpc (json_rpc::error &&)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+on_finish ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_unreachable ();
+}
+
+// Handling of requests from server
+
+void
+lsp::gcc_impl::worker::mediator::
+on_request_textDocument_documentSymbol (std::unique_ptr<json::value> req_id,
+					spec::DocumentSymbolParams &&p)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  gcc_assert (m_frontend);
+  /* FIXME: we can hit this with null frontend
+     if the request hits in the same loop as didOpen.
+     We need to stop processing the inputs immediately after
+     didOpen is processed.  */
+
+  auto symbols = m_frontend->get_symbols ();
+  send_response_to_server
+    (json_rpc::response::success (std::move (req_id),
+				  symbols.to_json ()));
+}
+
+// Handling of notifications from server
+
+void
+lsp::gcc_impl::worker::mediator::
+on_notification_textDocument_didOpen (lsp::spec::DidOpenTextDocumentParams &&p)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  if (logger)
+    logger->log ("didOpen: uri:\"%s\"", p.textDocument.uri.m_val.c_str ());
+
+  auto file_uri = uris::parse_as_file_uri (p.textDocument.uri.m_val);
+  if (!file_uri)
+    {
+      // FIXME:
+      gcc_unreachable ();
+    }
+  if (!file_uri->local_p ())
+    {
+      // FIXME:
+      gcc_unreachable ();
+    }
+
+  m_filename_from_didOpen = xstrdup (file_uri->m_path_absolute.c_str ());
+
+  /* For ease of debugging, generate a process title indicating the doc
+     URI and version.
+     Linux truncates this to 16 chars, so pre-truncate it to the right
+     so that the most important information for distinguishing different
+     worker process is retained.  */
+  {
+    const size_t max_process_title_len = 15;
+    std::string process_title
+      = file_uri->m_path_absolute + "@" + std::to_string (p.textDocument.version.m_val);
+    // FIXME:
+    if (process_title.size () > max_process_title_len)
+      process_title = (std::string ("...")
+		       + std::string ((process_title.end ()
+				       - (max_process_title_len - 3)),
+				      process_title.end ()));
+    setproctitle (process_title.c_str ());
+  }
+
+  // FIXME: similarly for logger title?
+
+  // FIXME: stash the content and override the filesystem
+  {
+    /* FIXME: this is specific to c-family; we'll
+       presumably want a langhook for this.  */
+    gcc_assert (parse_in);
+    cpp_reader_override_file_contents (parse_in,
+				       file_uri->m_path_absolute.c_str (),
+				       p.textDocument.text.m_val.c_str ());
+  }
+}
+
+
+void
+lsp::gcc_impl::worker::mediator::dump_ast ()
+{
+  /* FIXME: this will send it as a diagnostic, which is probably not want we want,
+     as it will get intercepted.  */
+  // might need a custom context and sink?
+  
+  auto_diagnostic_group d;
+  inform (UNKNOWN_LOCATION, "AST nodes");
+  for (auto &iter : m_recorder.m_root_node.m_children)
+    visit_node (*iter);
+}
+
+void
+lsp::gcc_impl::worker::mediator::
+visit_node (const gcc::topics::ast_events::recorder::saved_node &node)
+{
+  // FIXME: see notes for dump_ast
+  auto_diagnostic_nesting_level sentinel;
+  inform (node.m_start_loc, "%qs", node.m_kind);
+  for (auto &iter : node.m_children)
+    visit_node (*iter);
+}
diff --git a/gcc/lsp/gcc-impl/worker.h b/gcc/lsp/gcc-impl/worker.h
new file mode 100644
index 00000000000..5f87ee3718c
--- /dev/null
+++ b/gcc/lsp/gcc-impl/worker.h
@@ -0,0 +1,140 @@
+/* FIXME.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_GCC_LSP_WORKER_H
+#define GCC_LSP_GCC_LSP_WORKER_H
+
+#include "lsp/gcc-impl/common.h"
+#include "lsp/lsp-spec.h"
+#include "lsp/frontend-interface.h"
+#include "lsp/octets-to-json-rpc.h"
+#include "topics/ast-events-recorder.h"
+
+namespace lsp {
+namespace gcc_impl {
+namespace worker {
+
+class mediator;
+namespace diagnostics { class sink; }
+
+#if 0
+class impl_worker_for_server : public lsp::noop_worker_impl_for_server
+{
+public:
+  impl_worker_for_server (mediator &mediator_) : m_mediator (mediator_) {}
+
+  void
+  notify_textDocument_didOpen (const lsp::spec::DidOpenTextDocumentParams &p) override;
+
+#if 0
+  /* Implementation of "textDocument/definition":
+     find the definition of the symbol at a given source location.  */
+  lsp::result<std::vector<lsp::spec::Location>>
+  on_textDocument_definition (const lsp::spec::TextDocumentPositionParams &p) override;
+#endif
+
+private:
+  mediator &m_mediator;
+};
+#endif
+
+class mediator : public json_rpc::sink
+{
+public:
+  mediator (lsp::logger *logger);
+
+  const char *
+  handle_events_until_didOpen ();
+
+  void
+  handle_events_after_compile (lsp::frontend_worker_interface *frontend);
+
+  bool done_compiling_p () const { return m_done_compiling_p; }
+
+  void
+  send_response_to_server (json_rpc::response r);
+
+  void
+  send_notification_to_server (json_rpc::notification n);
+
+  ipc::sender &
+  get_ipc_sender_for_server ()
+  {
+    return m_server_ipc_connection.m_outgoing_sender;
+  }
+
+  // Implementation of json_rpc::sink
+
+  void
+  consume_json_rpc_request (json_rpc::request &&) final override;
+
+  void
+  consume_json_rpc_notification (json_rpc::notification &&) final override;
+
+  void
+  consume_json_rpc_response (json_rpc::response &&) final override;
+
+  void
+  consume_json_rpc_error (json_rpc::error &&) final override;
+
+  void
+  on_malformed_json (json::error &&) final override;
+
+  void
+  on_invalid_json_rpc (json_rpc::error &&) final override;
+
+  void
+  on_finish () final override;
+
+  // Handling of requests from server
+
+  void
+  on_request_textDocument_documentSymbol (std::unique_ptr<json::value> req_id,
+					  spec::DocumentSymbolParams &&p);
+
+  // Handling of notifications from server
+
+  void
+  on_notification_textDocument_didOpen (lsp::spec::DidOpenTextDocumentParams &&);
+  
+  // FIXME:
+  // private:
+  void dump_ast ();
+  void visit_node (const gcc::topics::ast_events::recorder::saved_node &node);
+
+  ipc::connection m_server_ipc_connection;
+  octets_to_json_rpc m_octets_from_server;
+  char *m_filename_from_didOpen;
+
+  lsp::gcc_impl::worker::diagnostics::sink *m_lsp_diagnostic_sink;
+  gcc::topics::ast_events::recorder m_recorder;
+  bool m_done_compiling_p;
+
+  // Requests from server queued until compilation is done
+  std::vector<json_rpc::request> m_queued_requests;
+
+  lsp::frontend_worker_interface *m_frontend;
+};
+
+} // namespace lsp::gcc_impl::worker
+} // namespace lsp::gcc_impl
+} // namespace lsp
+
+#endif  /* GCC_LSP_GCC_LSP_WORKER_H  */
diff --git a/gcc/lsp/ipc.cc b/gcc/lsp/ipc.cc
new file mode 100644
index 00000000000..9f9a7b8d4dd
--- /dev/null
+++ b/gcc/lsp/ipc.cc
@@ -0,0 +1,343 @@
+/* Low-level IPC classes
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "lsp/octets.h"
+#include "lsp/ipc.h"
+
+// FIXME:
+#include <poll.h>
+
+static void
+log_buffer (lsp::logger &logger,
+	    const octets::buffer &buf)
+{
+  logger.emit_start_color ();
+  fprintf (stderr, "START OF QUOTE<<<");
+  logger.emit_stop_color ();
+  fwrite (buf.data (), buf.size (), 1, stderr);
+  logger.emit_start_color ();
+  fprintf (stderr, ">>>END OF QUOTE\n");
+  logger.emit_stop_color ();
+  fflush (stderr); // FIXME
+}
+
+// class ipc::endpoint : public lsp::log_user
+
+// class ipc::receiver : public endpoint
+
+void
+ipc::receiver::blocking_read ()
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (m_octet_sink);
+
+  octets::buffer buf (4096, 0);
+  ssize_t num_octets_read
+    = read (get_fd ().as_int (), buf.data (), buf.size ());
+  if (logger)
+    logger->log ("'read' returned %li for receiver fd %i (%s)",
+		 (long)num_octets_read, get_fd ().as_int (),  // FIXME
+		 get_debug_name ().c_str ());
+
+  if (num_octets_read == -1)
+    {
+      // error
+      gcc_unreachable ();
+    }
+
+  buf.resize (num_octets_read);
+  if (num_octets_read == 0)
+    {
+      if (logger)
+	logger->log ("EOF for receiver fd %i (%s)",
+		     get_fd ().as_int (),
+		     get_debug_name ().c_str ());
+      m_octet_sink->on_end_of_octet_stream ();
+      return;
+    }
+
+  if (logger)
+    {
+      logger->log ("read %li octets from receiver fd %i (%s)",
+		   (long)num_octets_read,
+		   get_fd ().as_int (),
+		   get_debug_name ().c_str ());
+      log_buffer (*logger, buf);
+    }
+
+  m_octet_sink->on_consume_octets (std::move (buf));
+}
+
+// class ipc::sender : public endpoint
+
+void
+ipc::sender::add_data_to_queue (octets::buffer data)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (!m_close_queued);
+  gcc_assert (!m_closed);
+
+  if (logger)
+    logger->log ("added %li octets to sender buffer for fd %i (%s)",
+		 (long)data.size (),
+		 get_fd ().as_int (),
+		 get_debug_name ().c_str ());
+  m_outgoing_buffer.insert<octets::buffer::iterator>
+    (m_outgoing_buffer.end (),
+     data.begin (),
+     data.end ());
+}
+
+void
+ipc::sender::add_close_to_queue ()
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (!m_close_queued);
+  gcc_assert (!m_closed);
+
+  if (logger)
+    logger->log ("queuing close of sender fd %i (%s)",
+		 get_fd ().as_int (),
+		 get_debug_name ().c_str ());
+
+  m_close_queued = true;
+}
+
+void
+ipc::sender::add_test_data_to_queue (const char *str)
+{
+  octets::buffer buf;
+  for (const char *iter = str; *iter; ++iter)
+    buf.push_back (*iter);
+  add_data_to_queue (buf);
+}
+
+void
+ipc::sender::blocking_send ()
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  gcc_assert (!m_closed);
+
+  if (m_outgoing_buffer.size () > 0)
+    {
+      ssize_t num_octets_written
+	= write (get_fd ().as_int (),
+		 m_outgoing_buffer.data (),
+		 m_outgoing_buffer.size ());
+      if (logger)
+	logger->log ("'write' returned %li for sender fd %i (%s)",
+		     (long)num_octets_written,  // FIXME
+		     get_fd ().as_int (),
+		     get_debug_name ().c_str ());
+      if (num_octets_written == -1)
+	{
+	  // Error
+	  gcc_unreachable (); // FIXME
+	}
+
+      if (logger)
+	{
+	  logger->log ("wrote %li octets to sender fd %i (%s)",
+		       (long)num_octets_written, // FIXME
+		       get_fd ().as_int (),
+		       get_debug_name ().c_str ());
+	  log_buffer (*logger, m_outgoing_buffer);
+	}
+
+      // Remove num_octets_written from front of vector
+      if (num_octets_written == (ssize_t)m_outgoing_buffer.size ())
+	m_outgoing_buffer.clear ();
+      else
+	gcc_unreachable (); // FIXME: partial write
+    }
+  else
+    {
+      gcc_assert (m_close_queued);
+      int close_result = close (get_fd ().as_int ());
+      if (logger)
+	logger->log ("'close' returned %i for sender fd %i (%s)",
+		     close_result, get_fd ().as_int (),
+		     get_debug_name ().c_str ());
+      // FIXME: error handling?
+      if (logger)
+	logger->log ("closed sender fd %i (%s)",
+		     get_fd ().as_int (),
+		     get_debug_name ().c_str ());
+      m_close_queued = false;
+      m_closed = true;
+    }
+}
+
+// class ipc::event_poll : public lsp::log_user
+
+ipc::event_poll::event_poll ()
+{
+}
+
+void
+ipc::event_poll::add_endpoint (endpoint *endpoint)
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  m_endpoints.push_back (endpoint);
+}
+
+void
+ipc::event_poll::handle_pending_events (bool block)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+  /* Build an array of pollfd with the filedescriptors we're interested in,
+     along with an array of the endpoints they correspond to.  */
+  std::vector<pollfd> pfds;
+  std::vector<endpoint *> endpoint_vec;
+  for (const auto &ep : m_endpoints)
+    {
+      switch (ep->get_direction ())
+	{
+	default:
+	  gcc_unreachable ();
+	case endpoint::direction::incoming:
+	  if (logger)
+	    logger->log ("considering receiver fd %i (%s)",
+			 ep->get_fd ().as_int (),
+			 ep->get_debug_name ().c_str ());
+	  pfds.push_back (pollfd {ep->get_fd ().as_int (),
+				  POLLIN,
+				  0});
+	  endpoint_vec.push_back (ep);
+	  break;
+	case endpoint::direction::outgoing:
+	  {
+	    ipc::sender *s = static_cast<ipc::sender *> (ep);
+	    if (s->has_queued_activity_p ())
+	      {
+		if (logger)
+		  logger->log ("considering sender fd %i (%s)",
+			       ep->get_fd ().as_int (),
+			       ep->get_debug_name ().c_str ());
+		pfds.push_back (pollfd {ep->get_fd ().as_int (),
+					POLLOUT | POLLHUP | POLLERR,
+					0});
+		endpoint_vec.push_back (ep);
+	      }
+	  }
+	  break;
+	}
+    }
+
+  // Invoke "poll".
+  const int timeout = block ? -1 : 0;
+  if (logger)
+    logger->log ("calling 'poll'");
+  int poll_result = poll (pfds.data(), pfds.size (), timeout);
+  if (logger)
+    logger->log ("'poll' returned %i", poll_result);
+
+  if (poll_result == -1)
+    {
+      // An error occurred
+      gcc_unreachable (); // FIXME
+    }
+
+  // Process any FDs that are ready
+  if (poll_result > 0)
+    {
+      gcc_assert (pfds.size () == endpoint_vec.size ());
+      for (size_t i = 0; i < pfds.size (); ++i)
+	{
+	  const auto &pfd = pfds[i];
+	  endpoint *ep = endpoint_vec[i];
+	  if (pfd.revents & POLLIN)
+	    {
+	      if (logger)
+		logger->log ("POLLIN for fd %i (%s)",
+			     pfds[i].fd,
+			     ep->get_debug_name ().c_str ());
+	      gcc_assert (ep->get_direction ()
+			  == endpoint::direction::incoming);
+	      ipc::receiver *r = static_cast<ipc::receiver *> (ep);
+	      r->blocking_read ();
+	    }
+	  if (pfd.revents & POLLHUP)
+	    {
+	      if (logger)
+		logger->log ("POLLHUP for fd %i (%s)",
+			     pfds[i].fd,
+			     ep->get_debug_name ().c_str ());
+	      gcc_assert (ep->get_direction ()
+			  == endpoint::direction::incoming);
+	      ipc::receiver *r = static_cast<ipc::receiver *> (ep);
+	      r->blocking_read (); // FIXME: should have a handler for this
+	    }
+	  if (pfd.revents & POLLERR)
+	    {
+	      if (logger)
+		logger->log ("POLLERR for fd %i (%s)",
+			     pfds[i].fd,
+			     ep->get_debug_name ().c_str ());
+	      gcc_assert (ep->get_direction ()
+			  == endpoint::direction::incoming);
+	      ipc::receiver *r = static_cast<ipc::receiver *> (ep);
+	      r->blocking_read (); // FIXME: should have a handler for this
+	    }
+	  if (pfd.revents & POLLOUT)
+	    {
+	      if (logger)
+		logger->log ("POLLOUT for fd %i (%s)",
+			     pfds[i].fd,
+			     ep->get_debug_name ().c_str ());
+	      gcc_assert (ep->get_direction ()
+			  == endpoint::direction::outgoing);
+	      ipc::sender *s = static_cast<ipc::sender *> (ep);
+	      s->blocking_send ();
+	    }
+	}
+    }
+}
+
+// class octets_to_ipc_sender : public octets::sink
+
+void
+octets_to_ipc_sender::on_consume_octets (octets::buffer data)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_ipc_sender);
+  m_ipc_sender->add_data_to_queue (std::move (data));
+}
+
+void
+octets_to_ipc_sender::on_end_of_octet_stream ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_ipc_sender);
+  m_ipc_sender->add_close_to_queue ();
+}
diff --git a/gcc/lsp/ipc.h b/gcc/lsp/ipc.h
new file mode 100644
index 00000000000..cc15eb87dfc
--- /dev/null
+++ b/gcc/lsp/ipc.h
@@ -0,0 +1,256 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+// FIXME: rename:
+#ifndef GCC_LSP_IPC_H
+#define GCC_LSP_IPC_H
+
+#include "lsp/octets.h"
+
+/* Wrapper aroung "int" for file descriptors.  */
+
+class file_descriptor
+{
+public:
+  explicit file_descriptor (int fd) : m_int (fd) {}
+
+  int as_int () const { return m_int; }
+
+private:
+  int m_int;
+};
+
+namespace ipc {
+
+/* Wrapper around a file-descriptor, for use by selector.  */
+
+class endpoint : public lsp::log_user
+{
+public:
+  enum class direction { incoming, outgoing };
+
+  endpoint (file_descriptor fd, direction dir)
+  : m_fd (fd),
+    m_dir (dir)
+  {}
+
+  file_descriptor get_fd () const { return m_fd; }
+  direction get_direction () const { return m_dir; }
+
+  void
+  set_debug_name (std::string name)
+  {
+    m_debug_name = name;
+  }
+
+  const std::string &
+  get_debug_name () const { return m_debug_name; }
+
+private:
+  file_descriptor m_fd;
+  direction m_dir;
+  std::string m_debug_name;
+};
+
+/* A file descriptor we're reading from, with a octet sink to
+   send incoming data to.  */
+
+class receiver : public endpoint
+{
+public:
+  receiver (file_descriptor fd)
+  : endpoint (fd, direction::incoming),
+    m_octet_sink (nullptr)
+  {}
+
+  void
+  set_octet_sink (octets::sink *octet_sink) { m_octet_sink = octet_sink; }
+
+  void blocking_read ();
+
+private:
+  octets::sink *m_octet_sink;
+};
+
+/* A file descriptor we're writing to.
+   Data is buffered until the fd is ready for writing.  */
+
+class sender : public endpoint
+{
+public:
+  sender (file_descriptor fd)
+  : endpoint (fd, direction::outgoing),
+    m_close_queued (false),
+    m_closed (false)
+  {
+  }
+
+  bool
+  has_queued_activity_p () const
+  {
+    return m_close_queued || !m_outgoing_buffer.empty ();
+  }
+
+  void add_data_to_queue (octets::buffer data);
+  void add_close_to_queue ();
+
+  void add_test_data_to_queue (const char *data);
+
+  void blocking_send ();
+
+private:
+  octets::buffer m_outgoing_buffer;
+  bool m_close_queued;
+  bool m_closed;
+};
+
+/* FIXME: wrapper around poll, for monitoring a collection of endpoints.  */
+
+class event_poll : public lsp::log_user
+{
+public:
+  event_poll ();
+
+  void
+  add_endpoint (endpoint *endpoint);
+
+  void
+  handle_pending_events (bool block);
+
+#if 0
+  void
+  handle_events_forever ();
+#endif
+
+private:
+  std::list<endpoint *> m_endpoints;
+};
+
+// FIXME: not yet used
+/* A two-way connection.  */
+
+class connection
+{
+public:
+  connection (file_descriptor outgoing_fd,
+	      file_descriptor incoming_fd)
+  : m_outgoing_sender (outgoing_fd),
+    m_incoming_receiver (incoming_fd)
+  {
+  }
+
+  void
+  set_logger (lsp::logger *logger)
+  {
+    m_outgoing_sender.set_logger (logger);
+    m_incoming_receiver.set_logger (logger);
+  }
+
+  void
+  queue_outgoing_data (octets::buffer data)
+  {
+    m_outgoing_sender.add_data_to_queue (std::move (data));
+  }
+
+  void
+  queue_outgoing_close ();
+
+  void
+  set_incoming_octet_sink (octets::sink *octet_sink)
+  {
+    m_incoming_receiver.set_octet_sink (octet_sink);
+  }
+
+// FIXME:
+//private:
+  sender m_outgoing_sender;
+  receiver m_incoming_receiver;
+};
+
+#if 0
+// FIXME: an octets::connection implemented in terms of an underlying ipc::connection
+
+class ipc_connection : public connection
+{
+};
+#endif
+
+} // namespace ipc
+
+/* Concrete implementation of octets::sink which sends
+   the data to an ipc::sender.  */
+
+class octets_to_ipc_sender : public octets::sink
+{
+public:
+  octets_to_ipc_sender () : m_ipc_sender (nullptr)
+  {
+  }
+
+  void
+  set_ipc_sender (ipc::sender *ipc_sender)
+  {
+    m_ipc_sender = ipc_sender;
+  }
+    
+  void on_consume_octets (octets::buffer data) final override;
+  void on_end_of_octet_stream () final override;
+
+private:
+  ipc::sender *m_ipc_sender;
+};
+
+// FIXME:
+/* Connects a two-way ipc::connection with a two-way octets::connection.  */
+
+class ipc_octets_adaptor
+{
+public:
+  ipc_octets_adaptor (ipc::connection *ipc_connection,
+		       octets::connection *octets_connection)
+  : m_ipc_connection (ipc_connection),
+    m_octets_connection (octets_connection)
+  {
+    gcc_assert (m_ipc_connection);
+    gcc_assert (m_octets_connection);
+#if 0
+    ipc_connection->set_incoming_octet_sink (&m_octets_http_adaptor.m_incoming_octets_to_http);
+#endif
+  }
+
+  void
+  set_logger (lsp::logger *logger)
+  {
+#if 0
+    m_incoming_ipc_to_octets.set_logger (logger);
+    m_outgoing_octets_to_ipc.set_logger (logger);
+#endif
+  }
+
+private:
+  ipc::connection *m_ipc_connection;
+  octets::connection *m_octets_connection;
+#if 0
+  ipc_to_octets m_incoming_ipc_to_octets;
+  octets_to_ipc m_outgoing_octets_to_ipc;
+#endif
+};
+
+#endif  /* GCC_LSP_IPC_H  */
diff --git a/gcc/lsp/json-rpc.cc b/gcc/lsp/json-rpc.cc
new file mode 100644
index 00000000000..3f3ce881469
--- /dev/null
+++ b/gcc/lsp/json-rpc.cc
@@ -0,0 +1,616 @@
+/* JSON-RPC server implementation.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_STRING
+#define INCLUDE_MAP
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "json-parsing.h"
+#include "pretty-print.h"
+#include "lsp/mini-http.h"
+#include "lsp/json-rpc.h"
+#include "selftest.h"
+
+std::unique_ptr<json::object>
+json_rpc::request::to_json (request &&req)
+{
+  auto json_obj = std::make_unique<json::object> ();
+  json_obj->set_string ("jsonrpc", "2.0");
+  json_obj->set_string ("method", req.get_method ());
+  json_obj->set ("params", std::move (req.m_params));;
+  json_obj->set ("id", req.take_id ());
+  return json_obj;
+}
+
+std::unique_ptr<json::object>
+json_rpc::notification::to_json (notification &&n)
+{
+  auto json_obj = std::make_unique<json::object> ();
+  json_obj->set_string ("jsonrpc", "2.0");
+  json_obj->set_string ("method", n.m_method);
+  json_obj->set ("params", std::move (n.m_params));;
+  return json_obj;
+}
+
+/* Create a JSON-RPC 2.0 error object from ERR.  */
+
+std::unique_ptr<json::object>
+json_rpc::error::to_json (error &&err)
+{
+  auto js_error = std::make_unique<json::object> ();
+  js_error->set_integer ("code", err.m_code);
+  js_error->set_string ("message", err.m_message.c_str ());
+  if (err.m_data)
+    js_error->set ("data", std::move (err.m_data));
+  return js_error;
+}
+
+json_rpc::error
+json_rpc::parse_error (const char *msg)
+{
+  if (msg == nullptr)
+    msg = "parse error";
+  return error (PARSE_ERROR, msg, nullptr);
+}
+
+json_rpc::error
+json_rpc::invalid_request (const char *msg)
+{
+  if (msg == nullptr)
+    msg = "invalid request";
+  return error (INVALID_REQUEST, msg, nullptr);
+}
+
+json_rpc::error
+json_rpc::method_not_found (const char *method)
+{
+  const char *msg = ACONCAT (("method not found: ", method, nullptr));
+  return error (METHOD_NOT_FOUND, msg, nullptr);
+}
+
+json_rpc::error
+json_rpc::invalid_params (const char *msg)
+{
+  if (msg == nullptr)
+    msg = "invalid params";
+  return error (INVALID_PARAMS, msg, nullptr);
+}
+
+/* Create a JSON-RPC 2.0 response object for request ID, with result
+   as RESULT, taking ownership of RESULT.  */
+
+json_rpc::response
+json_rpc::response::success (std::unique_ptr<json::value> req_id,
+                            std::unique_ptr<json::value> result)
+{
+  return response (std::move (result), std::move (req_id));
+}
+/* A C++ class for a JSON-RPC response.  */
+
+json_rpc::error
+json_rpc::error::invalid_params (std::unique_ptr<json::value> req_id,
+				 std::string msg)
+{
+  return error (INVALID_PARAMS, std::move (msg),
+		req_id ? req_id->clone () : nullptr);
+}
+
+/* Create a JSON-RPC 2.0 response object from RESP.  */
+
+std::unique_ptr<json::object>
+json_rpc::response::to_json (response &&resp)
+{
+  auto js_obj = std::make_unique<json::object> ();
+  js_obj->set_string ("jsonrpc", "2.0");
+  if (resp.m_result_val)
+    {
+      js_obj->set ("result", std::move (resp.m_result_val));
+      gcc_assert (!resp.m_error_obj);
+    }
+  else
+    {
+      gcc_assert (resp.m_error_obj);
+      js_obj->set ("error", std::move (resp.m_error_obj));
+    }
+  if (resp.m_req_id)
+    js_obj->set ("id", std::move (resp.m_req_id));
+  return js_obj;
+}
+
+#if 0
+json_rpc::response
+json_rpc::response::parse_error (const char *msg)
+{
+  return failure (PARSE_ERROR, msg, nullptr);
+}
+
+json_rpc::response
+json_rpc::response::invalid_request (const json::value *req_id,
+				     const char *msg)
+{
+  if (msg == nullptr)
+    msg = "invalid request";
+  return failure (INVALID_REQUEST, msg, req_id);
+}
+#endif
+
+json_rpc::response
+json_rpc::response::method_not_found (request &&req)
+{
+  return failure (req.take_id (),
+		  json_rpc::method_not_found (req.get_method ()));
+}
+
+void
+json_rpc::sink::
+consume_json_message (json::value &&jv_msg)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+  if (logger)
+    {
+      logger->log ("consuming json::value: ");
+      jv_msg.dump (stderr, false); // FIXME
+      fprintf (stderr, "\n");
+    }
+
+#if 0
+  gcc_assert (invalid_json_sink);
+#endif
+
+  // TODO: handle batch requests?
+
+  if (jv_msg.get_kind () != json::JSON_OBJECT)
+    {
+      on_invalid_json_rpc (json_rpc::parse_error ("not an object"));
+      return;
+    }
+
+  json::object &msg_obj = *static_cast <json::object *> (&jv_msg);
+
+  /* Check version.  */
+  const json::value *version = msg_obj.get ("jsonrpc");
+  if (!version)
+    {
+      on_invalid_json_rpc
+	(json_rpc::invalid_request ("missing 'jsonrpc' version"));
+      return;
+    }
+  const json::string *version_string = version->dyn_cast_string ();
+  if (!version_string)
+    {
+      on_invalid_json_rpc
+	(json_rpc::invalid_request ("'jsonrpc' was not 2.0"));
+      return;
+    }
+  if (0 != strcmp (version_string->get_string (), "2.0"))
+    {
+      on_invalid_json_rpc
+	(json_rpc::invalid_request ("'jsonrpc' was not 2.0"));
+      return;
+    }
+
+  /* Take any id and borrow any method.  */
+  auto req_id = msg_obj.maybe_take ("id");
+  /* TODO: validate non-NULL id.  */
+
+  const json::value *method = msg_obj.get ("method");
+
+  if (method)
+    {
+      const json::string *method_str = method->dyn_cast_string ();
+      if (!method_str)
+	{
+	  on_invalid_json_rpc
+	    (json_rpc::invalid_request ("method was not a string"));
+	  return;
+	}
+      auto params = msg_obj.maybe_take ("params");
+      if (req_id.get ())
+	{
+	  /* "method" and "id": this is a JSON-RPC request.  */
+	  if (logger)
+	    logger->log ("JSON-RPC call(FIXME): \"%s\"",
+			 method_str->get_string ());
+	  json_rpc::request req (method_str->get_string (),
+				 std::move (params),
+				 std::move (req_id));
+	  consume_json_rpc_request (std::move (req));
+	}
+      else
+	{
+	  /* "method" without "id": this is a JSON-RPC notification.  */
+	  if (logger)
+	    logger->log ("JSON-RPC notification: \"%s\"",
+			 method_str->get_string ());
+	  json_rpc::notification notification (method_str->get_string (),
+					       std::move (params));
+	  consume_json_rpc_notification
+	    (std::move (notification));
+	}
+    }
+  else
+    {
+      if (req_id.get ())
+	{
+	  /* "id" without "method": this is a JSON-RPC response.  */
+	  if (logger)
+	    logger->log ("JSON-RPC response(FIXME)");
+	  auto result = msg_obj.maybe_take ("result");
+	  auto resp = json_rpc::response::success (std::move (req_id),
+						   std::move (result));
+	  consume_json_rpc_response (std::move (resp));
+	}
+      else
+	{
+	  /* neither of "id" or "method": this is a JSON-RPC error.  */
+	  gcc_unreachable (); // FIXME
+	}
+    }
+}
+
+void
+json_rpc::send_notification_to_ipc_sender (json_rpc::notification n,
+					   ipc::sender &sender)
+{
+  send_json_message_to_ipc_sender
+    (json_rpc::notification::to_json (std::move (n)),
+     sender);
+}
+
+void
+json_rpc::send_json_message_to_ipc_sender (std::unique_ptr<json::object> js_obj,
+					   ipc::sender &sender)
+{
+  pretty_printer pp;
+  js_obj->print (&pp, true); // FIXME: m_format_json);
+
+  std::vector<uint8_t> msg_content;
+  for (const char *ch = pp_formatted_text (&pp); *ch; ++ch)
+    msg_content.push_back ((uint8_t)*ch);
+
+  mini_http::message msg (std::move (msg_content));
+  msg.send_to_ipc_sender (sender);
+}
+
+#if CHECKING_P
+
+// FIXME
+
+void
+::json_rpc::selftest::assert_response_dump_eq (const ::selftest::location &loc,
+					       ::json_rpc::response &&response_,
+					       const char *expected_json)
+{
+  auto js_obj = json_rpc::response::to_json (std::move (response_));
+  pretty_printer pp;
+  js_obj->print (&pp, true);
+  ASSERT_STREQ_AT (loc, pp_formatted_text (&pp), expected_json);
+}
+
+namespace selftest {
+
+/* Selftests.  */
+
+#if 0
+/* Assert that RESPONSE is a non-NULL response for ID.
+   Verify that the jsonrpc value is "2.0".
+   Return RESPONSE, cast to an object *.  */
+
+static const json::object *
+assert_is_response (const json::value *response, int id)
+{
+  ASSERT_TRUE (response != nullptr);
+  const json::object *obj = response->dyn_cast_object ();
+  ASSERT_TRUE (obj != nullptr);
+  json::value *jsonrpc = obj->get ("jsonrpc");
+  ASSERT_TRUE (jsonrpc != nullptr);
+  ASSERT_STREQ ("2.0", jsonrpc->dyn_cast_string ()->get_string ());
+  ASSERT_EQ (id, obj->get ("id")->dyn_cast_integer_number ()->get ());
+  return obj;
+}
+
+/* Assert that RESPONSE is a non-NULL response, with no ID.
+   Verify that the jsonrpc value is "2.0".
+   Return RESPONSE, cast to an object *.  */
+
+static const json::object *
+assert_is_response (const json::value *response)
+{
+  ASSERT_TRUE (response != nullptr);
+  const json::object *obj = response->dyn_cast_object ();
+  ASSERT_TRUE (obj != nullptr);
+  json::value *jsonrpc = obj->get ("jsonrpc");
+  ASSERT_TRUE (jsonrpc != nullptr);
+  ASSERT_STREQ ("2.0", jsonrpc->dyn_cast_string ()->get_string ());
+  return obj;
+}
+
+/* Assert that RESPONSE is a non-NULL successful response for ID.
+   Verify that the jsonrpc value is "2.0".
+   Return the "result" value.  */
+
+static const json::value *
+assert_is_success (const json::value *response, int id)
+{
+  const json::object *obj = assert_is_response (response, id);
+  ASSERT_EQ (nullptr, obj->get ("error"));
+  return obj->get ("result");
+}
+
+/* Assert that RESPONSE is an error response for ID.
+   Verify that CODE and MESSAGE match the given values.  */
+
+static void
+assert_is_failure (const json_rpc::response &response_,
+		   int code, const char *message, int id)
+{
+  const json::object *obj = assert_is_response (response_, id);
+  ASSERT_EQ (nullptr, obj->get ("result"));
+  const json::object *err = obj->get ("error")->dyn_cast_object ();
+  ASSERT_EQ (code, err->get ("code")->dyn_cast_integer_number ()->get ());
+  ASSERT_STREQ (message, err->get ("message")->dyn_cast_string ()->get_string ());
+}
+
+/* Assert that RESPONSE is an error response with no ID.
+   Verify that CODE and MESSAGE match the given values.  */
+
+static void
+assert_is_failure (const json_rpc::response &response_,
+		   int code, const char *message)
+{
+  const json::object *obj = assert_is_response (response_);
+  ASSERT_EQ (nullptr, obj->get ("result"));
+  const json::object *err = obj->get ("error")->dyn_cast_object ();
+  ASSERT_EQ (code, err->get ("code")->dyn_cast_integer_number ()->get ());
+  ASSERT_STREQ (message, err->get ("message")->dyn_cast_string ()->get_string ());
+}
+#endif
+
+using namespace json_rpc;
+
+namespace {
+
+#if 0
+/* A concrete subclass of json_rpc::request_handler for use by the selftests.  */
+
+class test_request_handler : public json_rpc::endpoint
+{
+public:
+  response
+  handle_json_rpc_request (request &&req) final override
+  {
+    if (0 == strcmp (req.get_method (), "subtract"))
+      return do_subtract (*req.get_params (), req.take_id ());
+    return response::method_not_found (std::move (req));
+  }
+
+  void
+  handle_json_rpc_notification (json_rpc::notification &&notification) final override
+  {
+    // no-op; FIXME: maybe accumulate them?
+  }
+
+private:
+  response
+  do_subtract (const json::value &params,
+	       std::unique_ptr<json::value> req_id)
+  {
+    const json::array *positional = params.dyn_cast_array ();
+    if (!positional)
+      return response::failure (std::move (req_id),
+				invalid_params ("params was not an array"));
+    if (positional->length () != 2)
+      return response::failure (std::move (req_id),
+				invalid_params ("length of params was not 2"));
+
+    const json::integer_number *arg0
+      = positional->get (0)->dyn_cast_integer_number ();
+    if (!arg0)
+      return response::failure (std::move (req_id),
+				invalid_params ("param 0 was not an integer"));
+    const json::integer_number *arg1
+      = positional->get (1)->dyn_cast_integer_number ();
+    if (!arg1)
+      return response::failure (std::move (req_id),
+				invalid_params ("param 1 was not an integer"));
+
+    long result = arg0->get () - arg1->get ();
+
+    return response::success (std::move (req_id),
+			      std::make_unique<json::integer_number> (result));
+  }
+};
+#endif
+
+#if 0
+class test_json_rpc_string_request
+{
+public:
+  test_json_rpc_string_request (const char *request_str,
+				json_rpc::endpoint *endpoint)
+  {
+    json_value_to_json_rpc m_incoming_json_value_to_json_rpc;
+    malformed_json_to_json_rpc m_incoming_malformed_json_to_json_rpc;
+    json_rpc_to_endpoint_impl m_incoming_json_rpc_to_endpoint_impl;
+
+    m_incoming_json_value_to_json_rpc.set_json_rpc_sink (&incoming_json_rpc_to_endpoint_impl);
+    m_incoming_json_rpc_to_endpoint_impl.set_endpoint (endpoint);
+    m_incoming_json_rpc_to_endpoint_impl.set_result_value_sink (&m_outgoing_json_value_to_http);
+  }
+
+};
+#endif
+
+} // anonymous namespace
+
+/* Verify that a simple JSON-RPC call (of "subtract") works.  */
+
+#if 0
+static void
+test_simple ()
+{
+  test_request_handler endpoint;
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+       " \"params\": [42, 23], \"id\": 1}");
+  auto response_ = handle_string_as_json_rpc_request (request, &endpoint);
+
+  ASSERT_EQ (response_.get_id ()->dyn_cast_integer_number ()->get (),
+	     1);
+  ASSERT_EQ (response_.get_result ()->dyn_cast_integer_number ()->get (),
+	     19);
+  ASSERT_EQ (response_.get_error (), nullptr);
+
+  ASSERT_JSON_RPC_RESPONSE_DUMP_EQ (std::move (response_),
+				    ("{\"jsonrpc\": \"2.0\",\n"
+				     " \"result\": 19,\n"
+				     " \"id\": 1}"));
+}
+#endif
+
+// FIXME:
+#if 0
+/* Verify that json_rpc::request_handler::make_invalid_params works.  */
+
+static void
+test_bad_params ()
+{
+  test_request_handler s;
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+       " \"params\": [42, \"0\"], \"id\": 1}");
+  auto response_ = s.handle_raw_string (request);
+  assert_is_failure (response_, INVALID_PARAMS, "param 1 was not a number", 1);
+  delete response;
+}
+
+/* Verify that json_rpc::request_handler enforces JSON-RPC 2.0.  */
+
+static void
+test_bad_version ()
+{
+  test_request_handler s;
+  const char *request
+    = ("{\"jsonrpc\": \"1.0\", \"method\": \"subtract\","
+       " \"params\": [42, 23], \"id\": 1}");
+  json::value *response = s.handle_raw_string (request);
+  assert_is_failure (response, INVALID_REQUEST, "jsonrpc was not 2.0", 1);
+  delete response;
+}
+
+/* Verify that json_rpc::request_handler handles non-string methods.  */
+
+static void
+test_method_not_a_string ()
+{
+  test_request_handler s;
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": 1,"
+       " \"params\": \"bar\", \"id\": 1}");
+  json::value *response = s.handle_raw_string (request);
+  assert_is_failure (response, INVALID_REQUEST, "method was not a string", 1);
+  delete response;
+}
+
+/* Verify that json_rpc::request_handler::make_method_not_found works.  */
+
+static void
+test_method_not_found ()
+{
+  test_request_handler s;
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"does_not_exist\","
+       " \"params\": [42, 23], \"id\": 1}");
+  json::value *response = s.handle_raw_string (request);
+  assert_is_failure (response, METHOD_NOT_FOUND,
+    "method not found: does_not_exist", 1);
+  delete response;
+}
+
+/* Verify that json_rpc::request_handler::handle_raw_string gracefully
+   handles malformed JSON.  */
+
+static void
+test_malformed_json ()
+{
+  test_request_handler s;
+  const char *request = "{";
+  json::value *response = s.handle_raw_string (request);
+  assert_is_failure (response, PARSE_ERROR,
+    "error at index 1: expected string for object key");
+  delete response;
+}
+#endif
+
+/* Verify that json_rpc::incoming_http_request_handler can handle JSON-RPC within HTTP,
+   delegating to a json_rpc::request_handler.  */
+
+static void
+test_http_request_handler ()
+{
+#if 0
+  mini_http::message msg;
+  const char *in = ("Content-Length: 69\r\n"
+		    "\r\n"
+		    "{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+		    " \"params\": [42, 23], \"id\": 1}");
+  bytes_to_http FIXME;
+  FIXME.set_http_sink ();
+
+  size_t consumed = req.parse_buffer (strlen (in), in);
+  ASSERT_EQ (strlen (in), consumed);
+
+  test_request_handler s;
+  json_rpc::incoming_http_request_handler http_request_handler (s);
+  old_http::response resp;
+  http_request_handler.on_http_request (req, resp);
+  ASSERT_STREQ (resp.get_content (),
+		"{\"jsonrpc\": \"2.0\", \"result\": 19, \"id\": 1}");
+  // FIXME: length?
+#endif
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+lsp_json_rpc_cc_tests ()
+{
+  // FIXME: get these working again
+#if 0
+  test_simple ();
+#endif
+#if 0
+  test_bad_params ();
+  test_bad_version ();
+  test_method_not_a_string ();
+  test_method_not_found ();
+  test_malformed_json ();
+#endif
+  test_http_request_handler ();
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/lsp/json-rpc.h b/gcc/lsp/json-rpc.h
new file mode 100644
index 00000000000..a1f0aba9d46
--- /dev/null
+++ b/gcc/lsp/json-rpc.h
@@ -0,0 +1,305 @@
+/* JSON-RPC.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_JSON_RPC_H
+#define GCC_LSP_JSON_RPC_H
+
+#include "lsp/ipc.h"
+#include "json.h"
+#include "json-parsing.h"
+
+namespace json_rpc
+{
+
+/* A C++ class for a JSON-RPC request.  */
+
+class request
+{
+public:
+  request (std::string method,
+	   std::unique_ptr<json::value> params,
+	   std::unique_ptr<json::value> id)
+  : m_method (std::move (method)),
+    m_params (std::move (params)),
+    m_id (std::move (id))
+  {
+    // params can be null
+    gcc_assert (m_id);
+  }
+
+  static std::unique_ptr<json::object>
+  to_json (request &&);
+
+#if 0
+  void dump (FILE *out, int indent) const;
+  void DEBUG_FUNCTION dump () const { dump (stderr, 0); }
+#endif
+
+  const char *get_method () const { return m_method.c_str (); }
+  const json::value *get_params () const { return m_params.get (); }
+  const json::value *get_id () const { return m_id.get (); }
+
+#if 0
+  std::unique_ptr<json::value>
+  take_params () { return std::move (m_params); }
+#endif
+
+  std::unique_ptr<json::value>
+  take_id () { return std::move (m_id); }
+
+private:
+  std::string m_method;
+  std::unique_ptr<json::value> m_params;
+  std::unique_ptr<json::value> m_id;
+};
+
+/* A C++ class for a JSON-RPC notification.  */
+
+class notification
+{
+public:
+  notification (const char *method,
+		std::unique_ptr<json::value> params)
+  : m_method (method),
+    m_params (std::move (params))
+  {
+    gcc_assert (m_method);
+  }
+
+  static std::unique_ptr<json::object>
+  to_json (notification &&);
+
+#if 0
+  void dump (FILE *out, int indent) const;
+  void DEBUG_FUNCTION dump () const { dump (stderr, 0); }
+#endif
+
+  const char *get_method () const { return m_method; }
+  const json::value *get_params () const { return m_params.get (); }
+
+#if 0
+  std::unique_ptr<json::value>
+  take_params () { return std::move (m_params); }
+#endif
+
+private:
+  const char *m_method;
+  std::unique_ptr<json::value> m_params;
+};
+
+/* A C++ class for the "error" part of a JSON-RPC response.  */
+
+class error
+{
+public:
+  static std::unique_ptr<json::object>
+  to_json (error &&);
+
+  error (int code, std::string message, std::unique_ptr<json::value> data)
+  : m_code (code),
+    m_message (std::move (message)),
+    m_data (std::move (data))
+  {
+  }
+
+#if 0
+  void dump (FILE *out, int indent) const;
+  void DEBUG_FUNCTION dump () const { dump (stderr, 0); }
+#endif
+
+  static error
+  invalid_params (std::unique_ptr<json::value> req_id,
+		  std::string msg);
+
+  int get_code () const { return m_code; }
+  const char *get_message () const { return m_message.c_str (); }
+  const json::value *get_data () const { return m_data.get (); }
+
+private:
+  int m_code;
+  std::string m_message;
+  std::unique_ptr<json::value> m_data;
+};
+
+/* The standard JSON-RPC error codes.  */
+const int PARSE_ERROR = -32700;
+const int INVALID_REQUEST = -32600;
+const int METHOD_NOT_FOUND = -32601;
+const int INVALID_PARAMS = -32602;
+const int INTERNAL_ERROR = -32603;
+
+extern error
+parse_error (const char *msg);
+
+extern error
+invalid_request (const char *msg);
+
+extern error
+method_not_found (const char *method);
+
+extern error
+invalid_params (const char *msg);
+
+/* A C++ class for a JSON-RPC response.  */
+
+class response
+{
+public:
+#if 0
+  void dump (FILE *out, int indent) const;
+  void DEBUG_FUNCTION dump () const { dump (stderr, 0); }
+#endif
+
+  static std::unique_ptr<json::object>
+  to_json (response &&);
+
+  static response
+  failure (std::unique_ptr<json::value> req_id,
+	   error &&err)
+  {
+    return response (std::move (err), std::move (req_id));
+  }
+
+  static response
+  method_not_found (request &&req);
+
+  static response
+  success (std::unique_ptr<json::value> req_id,
+	   std::unique_ptr<json::value> result);
+
+  static response
+  notification ()
+  {
+    // Placeholder response value for notifications:
+    return json_rpc::response (nullptr, nullptr);
+  }
+
+  const json::value *
+  get_result () const { return m_result_val.get (); };
+
+  const json::object *
+  get_error () const { return m_error_obj.get (); }
+
+  const json::value *
+  get_id () const { return m_req_id.get (); }
+
+private:
+  response (std::unique_ptr<json::value> result_val,
+	    std::unique_ptr<json::value> req_id)
+  : m_result_val (std::move (result_val)),
+    m_error_obj (nullptr),
+    m_req_id (std::move (req_id))
+  {
+  }
+
+  response (error &&err,
+	    std::unique_ptr<json::value> req_id)
+  : m_result_val (nullptr),
+    m_error_obj (error::to_json (std::move (err))),
+    m_req_id (std::move (req_id))
+  {
+  }
+
+  // Exactly one of m_result_val and m_error_obj should be non-null
+  std::unique_ptr<json::value> m_result_val;
+  std::unique_ptr<json::object> m_error_obj;
+  std::unique_ptr<json::value> m_req_id;
+};
+
+// FIXME
+
+class sink : public lsp::log_user
+{
+public:
+  void
+  consume_json_message (json::value &&); // FIXME
+
+  virtual void
+  consume_json_rpc_request (request &&) = 0;
+
+  virtual void
+  consume_json_rpc_notification (notification &&) = 0;
+
+  virtual void
+  consume_json_rpc_response (response &&) = 0;
+
+  virtual void
+  consume_json_rpc_error (error &&) = 0;
+
+  virtual void
+  on_malformed_json (json::error &&) = 0;
+
+  virtual void
+  on_invalid_json_rpc (error &&) = 0;
+
+  virtual void
+  on_finish () = 0;
+};
+
+#if 0
+// FIXME: not yet used
+/* A two-way connection.  */
+
+struct connection : public lsp::log_user
+{
+#if 0
+  connection ()
+  : m_outgoing_http_sink (nullptr),
+    m_incoming_http_sink (nullptr)
+  {
+  }
+
+  void on_outgoing_message (std::unique_ptr<json::value> msg);
+
+  sink *m_outgoing_http_sink;
+  sink *m_incoming_http_sink;
+#endif
+};
+#endif
+
+extern void
+send_notification_to_ipc_sender (json_rpc::notification n,
+				 ipc::sender &sender);
+
+extern void
+send_json_message_to_ipc_sender (std::unique_ptr<json::object> js_obj,
+				 ipc::sender &sender);
+
+#if CHECKING_P
+
+namespace selftest {
+
+void
+assert_response_dump_eq (const ::selftest::location &loc,
+			 ::json_rpc::response &&response_,
+			 const char *expected_json);
+
+// FIXME
+
+#define ASSERT_JSON_RPC_RESPONSE_DUMP_EQ(RESPONSE, EXPECTED_JSON) \
+  json_rpc::selftest::assert_response_dump_eq ((SELFTEST_LOCATION), (RESPONSE), (EXPECTED_JSON))
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
+
+} // namespace json_rpc
+
+#endif  /* GCC_LSP_JSON_RPC_H  */
diff --git a/gcc/lsp/lsp-logging.cc b/gcc/lsp/lsp-logging.cc
new file mode 100644
index 00000000000..f56a94dbbac
--- /dev/null
+++ b/gcc/lsp/lsp-logging.cc
@@ -0,0 +1,189 @@
+/* Logging for LSP, taken from libgccjit: logging
+   Copyright (C) 2014-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_STRING
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "toplev.h" /* for print_version */
+
+#include "lsp/lsp-logging.h"
+
+namespace lsp {
+
+/* Implementation of class lsp::logger.  */
+
+/* The constructor for lsp::logger.  */
+
+logger::logger (FILE *f_out,
+		std::string prefix,
+		const char *start_color,
+		const char *stop_color)
+: m_refcount (0),
+  m_f_out (f_out),
+  m_prefix (std::move (prefix)),
+  m_indent_level (0),
+  m_log_refcount_changes (false),
+  m_start_color (start_color),
+  m_stop_color (stop_color)
+{
+  /* Begin the log by writing the GCC version.  */
+  emit_start_color ();
+  print_version (f_out, prefix.c_str (), false);
+  emit_stop_color ();
+}
+
+/* The destructor for lsp::logger, invoked via
+   the decref method when the refcount hits zero.
+   Note that we do not close the underlying FILE * (m_f_out).  */
+
+logger::~logger ()
+{
+  /* This should be the last message emitted.  */
+  log ("%s", __PRETTY_FUNCTION__);
+  gcc_assert (m_refcount == 0);
+}
+
+/* Increment the reference count of the lsp::logger.  */
+
+void
+logger::incref (const char *reason)
+{
+  m_refcount++;
+  if (m_log_refcount_changes)
+    log ("%s: reason: %s refcount now %i ",
+	 __PRETTY_FUNCTION__, reason, m_refcount);
+}
+
+/* Decrement the reference count of the lsp::logger,
+   deleting it if nothing is referring to it.  */
+
+void
+logger::decref (const char *reason)
+{
+  gcc_assert (m_refcount > 0);
+  --m_refcount;
+  if (m_log_refcount_changes)
+    log ("%s: reason: %s refcount now %i",
+	 __PRETTY_FUNCTION__, reason, m_refcount);
+  if (m_refcount == 0)
+    delete this;
+}
+
+/* Write a formatted message to the log, by calling the log_va method.  */
+
+void
+logger::log (const char *fmt, ...)
+{
+  va_list ap;
+  va_start (ap, fmt);
+  log_va (fmt, ap);
+  va_end (ap);
+}
+
+/* Write an indented line to the log file.
+
+   We explicitly flush after each line: if something crashes the process,
+   we want the logfile/stream to contain the most up-to-date hint about the
+   last thing that was happening, without it being hidden in an in-process
+   buffer.  */
+
+void
+logger::log_va (const char *fmt, va_list ap)
+{
+  emit_start_color ();
+  fprintf (m_f_out, "%s: ", m_prefix.c_str ());
+  for (int i = 0; i < m_indent_level; i++)
+    fputc (' ', m_f_out);
+  vfprintf (m_f_out, fmt, ap);
+  fprintf (m_f_out, "\n");
+  emit_stop_color ();
+  fflush (m_f_out);
+}
+
+/* Record the entry within a particular scope, indenting subsequent
+   log lines accordingly.  */
+
+void
+logger::enter_scope (const char *scope_name)
+{
+  log ("entering: %s", scope_name);
+  m_indent_level += 1;
+}
+
+/* Record the exit from a particular scope, restoring the indent level to
+   before the scope was entered.  */
+
+void
+logger::exit_scope (const char *scope_name)
+{
+  if (m_indent_level)
+    m_indent_level -= 1;
+  else
+    log ("(mismatching indentation)");
+  log ("exiting: %s", scope_name);
+}
+
+void
+logger::emit_start_color ()
+{
+  if (m_start_color)
+    fprintf (m_f_out, m_start_color);
+}
+
+void
+logger::emit_stop_color ()
+{
+  if (m_stop_color)
+    fprintf (m_f_out, m_stop_color);
+}
+
+/* Implementation of class lsp::log_user.  */
+
+/* The constructor for lsp::log_user.  */
+
+log_user::log_user (logger *logger) : m_logger (logger)
+{
+  if (m_logger)
+    m_logger->incref("log_user ctor");
+}
+
+/* The destructor for lsp::log_user.  */
+
+log_user::~log_user ()
+{
+  if (m_logger)
+    m_logger->decref("log_user dtor");
+}
+
+/* Set the logger for a lsp::log_user, managing the reference counts
+   of the old and new logger (either of which might be NULL).  */
+
+void
+log_user::set_logger (logger *logger)
+{
+  if (logger)
+    logger->incref ("log_user::set_logger");
+  if (m_logger)
+    m_logger->decref ("log_user::set_logger");
+  m_logger = logger;
+}
+
+} // namespace lsp
diff --git a/gcc/lsp/lsp-logging.h b/gcc/lsp/lsp-logging.h
new file mode 100644
index 00000000000..6da65a98894
--- /dev/null
+++ b/gcc/lsp/lsp-logging.h
@@ -0,0 +1,202 @@
+/* Logging for LSP, adapted from libgccjit: logging
+   Copyright (C) 2014-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef LSP_LSP_LOGGING_H
+#define LSP_LSP_LOGGING_H
+
+#ifdef GCC_VERSION
+#if GCC_VERSION >= 4001
+#define GNU_PRINTF(M, N) __attribute__ ((format (gnu_printf, (M), (N))))
+#else
+#define GNU_PRINTF(M, N)
+#endif
+#endif
+
+namespace lsp {
+
+/* FIXME: unify the various loggers:
+   jit/jit-logging.h
+   analyzer/logging.h
+   rust?
+   lsp/lsp-logging.h
+
+   But not diagnostics/logging.h, as this can't use pretty_printer.  */
+
+/* An lsp::logger encapsulates a logging stream: a way to send
+   lines of pertinent information to a FILE *.  */
+
+class logger
+{
+ public:
+  logger (FILE *f_out,
+	  std::string prefix,
+	  const char *start_color,
+	  const char *stop_color);
+  ~logger ();
+
+  void incref (const char *reason);
+  void decref (const char *reason);
+
+  void log (const char *fmt, ...)
+    GNU_PRINTF(2, 3);
+  void log_va (const char *fmt, va_list ap)
+    GNU_PRINTF(2, 0);
+
+  void enter_scope (const char *scope_name);
+  void exit_scope (const char *scope_name);
+
+  void emit_start_color ();
+  void emit_stop_color ();
+
+private:
+  int m_refcount;
+  FILE *m_f_out;
+  std::string m_prefix;
+  int m_indent_level;
+  bool m_log_refcount_changes;
+  const char *m_start_color;
+  const char *m_stop_color;
+};
+
+/* The class lsp::log_scope is an RAII-style class intended to make
+   it easy to notify a logger about entering and exiting the body of a
+   given function.  */
+
+class log_scope
+{
+public:
+  log_scope (logger *logger, const char *name);
+  ~log_scope ();
+
+ private:
+  logger *m_logger;
+  const char *m_name;
+};
+
+/* The constructor for lsp::log_scope.
+
+   The normal case is that the logger is NULL, in which case this should
+   be largely a no-op.
+
+   If we do have a logger, notify it that we're entering the given scope.
+   We also need to hold a reference on it, to avoid a use-after-free
+   when logging the cleanup of the owner of the logger.  */
+
+inline
+log_scope::log_scope (logger *logger, const char *name) :
+ m_logger (logger),
+ m_name (name)
+{
+  if (m_logger)
+    {
+      m_logger->incref ("log_scope ctor");
+      m_logger->enter_scope (m_name);
+    }
+}
+
+/* The destructor for lsp::log_scope; essentially the opposite of
+   the constructor.  */
+
+inline
+log_scope::~log_scope ()
+{
+  if (m_logger)
+    {
+      m_logger->exit_scope (m_name);
+      m_logger->decref ("log_scope dtor");
+    }
+}
+
+/* A lsp::log_user is something that potentially uses a
+   lsp::logger (which could be NULL).
+
+   The log_user class keeps the reference-count of a logger up-to-date.  */
+
+class log_user
+{
+ public:
+  log_user (logger *logger = nullptr);
+  ~log_user ();
+
+  logger * get_logger () const { return m_logger; }
+  void set_logger (logger * logger);
+
+  void log (const char *fmt, ...) const
+    GNU_PRINTF(2, 3);
+
+  void enter_scope (const char *scope_name);
+  void exit_scope (const char *scope_name);
+
+ private:
+  logger *m_logger;
+};
+
+/* A shortcut for calling log from a FIXME, handling the common
+   case where the underlying logger is NULL via a no-op.  */
+
+inline void
+log_user::log (const char *fmt, ...) const
+{
+  if (m_logger)
+    {
+      va_list ap;
+      va_start (ap, fmt);
+      m_logger->log_va (fmt, ap);
+      va_end (ap);
+    }
+}
+
+/* A shortcut for recording entry into a scope from a FIXME,
+   handling the common case where the underlying logger is NULL via
+   a no-op.  */
+
+inline void
+log_user::enter_scope (const char *scope_name)
+{
+  if (m_logger)
+    m_logger->enter_scope (scope_name);
+}
+
+/* A shortcut for recording exit from a scope from a FIXME,
+   handling the common case where the underlying logger is NULL via
+   a no-op.  */
+
+inline void
+log_user::exit_scope (const char *scope_name)
+{
+  if (m_logger)
+    m_logger->exit_scope (scope_name);
+}
+
+} // namespace lsp
+
+/* If the given logger is non-NULL, log entry/exit of this scope to
+   it, identifying it using __PRETTY_FUNCTION__.  */
+
+#define LSP_LOG_SCOPE(LOGGER) \
+  lsp::log_scope s (LOGGER, __PRETTY_FUNCTION__)
+
+/* If the given logger is non-NULL, log entry/exit of this scope to
+   it, identifying it using __func__.  */
+
+#define LSP_LOG_FUNC(LOGGER) \
+  lsp::log_scope s (LOGGER, __func__)
+
+#endif /* LSP_LSP_LOGGING_H */
diff --git a/gcc/lsp/lsp-spec.cc b/gcc/lsp/lsp-spec.cc
new file mode 100644
index 00000000000..17191bc21b9
--- /dev/null
+++ b/gcc/lsp/lsp-spec.cc
@@ -0,0 +1,947 @@
+/* Language Server Protocol implementation.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "lsp/lsp-spec.h"
+#include "pretty-print.h"
+#include "pretty-print-markup-json.h"
+#include "uris.h"
+#include "selftest.h"
+
+using namespace json_rpc;
+using namespace lsp;
+
+const char *
+lsp::unmarshaller::describe_kind (const json::value &jv)
+{
+  switch (jv.get_kind ())
+    {
+    default:
+      gcc_unreachable ();
+
+    case json::JSON_OBJECT:
+      return "object";
+    case json::JSON_ARRAY:
+      return "array";
+    case json::JSON_INTEGER:
+      return "integer";
+    case json::JSON_FLOAT:
+      return "floating-point number";
+    case json::JSON_TRUE:
+      return "'true'";
+    case json::JSON_FALSE:
+      return "'false'";
+    case json::JSON_NULL:
+      return "'null'";
+    }
+}
+
+#if 0
+static std::unique_ptr<json_rpc::error>
+make_error (json_rpc::error &&err)
+{
+  return std::make_unique<json_rpc::error> (std::move (err));
+}
+#endif
+
+template <typename ValueType>
+result<ValueType>
+not_an_object (const unmarshaller &ctxt,
+	       const char *expected_type_name,
+	       const json::value &jv)
+{
+  pretty_printer pp;
+  pp_markup::quoted_json_pointer e_js_pointer (jv);
+  pp_printf (&pp, "expected %s object for %e; got %s",
+	     expected_type_name, &e_js_pointer, ctxt.describe_kind (jv));
+  return ctxt.invalid_params<ValueType> (pp_formatted_text (&pp));
+}
+
+template <typename ValueType>
+result<ValueType>
+not_a_string (const unmarshaller &ctxt,
+	      const json::value &jv)
+{
+  pretty_printer pp;
+  pp_markup::quoted_json_pointer e_js_pointer (jv);
+  pp_printf (&pp, "expected string for %e; got %s",
+	     &e_js_pointer, ctxt.describe_kind (jv));
+  return ctxt.invalid_params<ValueType> (pp_formatted_text (&pp));
+}
+
+template <typename ValueType>
+result<ValueType>
+not_an_integer (const unmarshaller &ctxt,
+		const json::value &jv)
+{
+  pretty_printer pp;
+  pp_markup::quoted_json_pointer e_js_pointer (jv);
+  pp_printf (&pp, "expected integer for %e; got %s",
+	     &e_js_pointer, ctxt.describe_kind (jv));
+  return ctxt.invalid_params<ValueType> (pp_formatted_text (&pp));
+}
+
+template <typename ValueType>
+result<ValueType>
+missing_property (const unmarshaller &ctxt,
+		  const json::object &obj,
+		  const char *obj_type_name,
+		  const char *property_name)
+{
+  pretty_printer pp;
+  pp_markup::quoted_json_pointer e_js_pointer (obj);
+  pp_printf (&pp, "missing %qs property for %s object %e",
+	     property_name, obj_type_name, &e_js_pointer);
+  return ctxt.invalid_params<ValueType> (pp_formatted_text (&pp));
+}
+
+template <typename PropertyType>
+lsp::result<PropertyType>
+unmarshal_required_property (const unmarshaller &ctxt,
+			     const json::object &obj,
+			     const char *obj_type_name,
+			     const char *property_name)
+{
+  auto jv_prop = obj.get (property_name);
+  if (!jv_prop)
+    return missing_property<PropertyType> (ctxt, obj,
+					   obj_type_name,
+					   property_name);
+  return PropertyType::from_json (ctxt, *jv_prop);
+}
+
+template <typename PropertyType>
+lsp::result<optional<PropertyType>>
+unmarshal_optional_property (const unmarshaller &ctxt,
+			     const json::object &obj,
+			     const char *obj_type_name,
+			     const char *property_name)
+{
+  auto jv_prop = obj.get (property_name);
+  if (!jv_prop)
+    return optional<PropertyType> ();
+  auto from_json_result = PropertyType::from_json (ctxt, *jv_prop);
+  if (from_json_result.get_error ())
+    return from_json_result.take_error ();
+  return optional<PropertyType> (from_json_result.take_value ());
+}
+
+/* Macros for implementing demarshalling for a given type.  */
+
+/* FIXME.  */
+
+#define BEGIN_DEFINE_FROM_JSON(TYPE_NAME) \
+lsp::result<lsp::spec::TYPE_NAME> \
+lsp::spec::TYPE_NAME::from_json (const unmarshaller &ctxt, \
+				 const json::value &jv) \
+{							\
+   const char *const type_name = #TYPE_NAME;				\
+  const json::object *obj = jv.dyn_cast_object ();			\
+    if (!obj)								\
+      return not_an_object<TYPE_NAME> (ctxt, type_name, jv);		\
+    TYPE_NAME unmarshalled_val;
+
+
+/* FIXME: implicitly uses "type_name", "ctxt", "*obj", "unmarshalled_val".  */
+
+#define UNMARSHAL_REQUIRED_PROPERTY(PROP_TYPE, PROP_NAME)	\
+  do { \
+    auto r = unmarshal_required_property<PROP_TYPE>			\
+      (ctxt, *obj, type_name, #PROP_NAME);				\
+    if (r.get_error ())						\
+      return r.take_error ();						\
+    unmarshalled_val.PROP_NAME = r.take_value ();			\
+  } while (0)
+
+#define UNMARSHAL_OPTIONAL_PROPERTY(PROP_TYPE, PROP_NAME)	\
+  do {								\
+    auto r = unmarshal_optional_property<PROP_TYPE>			\
+      (ctxt, *obj, type_name, #PROP_NAME);				\
+    if (r.get_error ())						\
+      return r.take_error ();						\
+    unmarshalled_val.PROP_NAME = r.take_value ();			\
+  } while (0)
+
+/* FIXME.  */
+
+#define END_DEFINE_FROM_JSON \
+  return unmarshalled_val;   \
+}
+
+
+/* Macros for implementing marshalling for a given type.  */
+
+/* FIXME.  */
+
+#define BEGIN_DEFINE_TO_JSON(TYPE_NAME) \
+  std::unique_ptr<json::value>					\
+  lsp::spec::TYPE_NAME::to_json () const			\
+  {								\
+    auto marshalled_obj = std::make_unique<json::object> ();
+
+
+// FIXME
+
+#define MARSHAL_REQUIRED_PROPERTY(PROP_TYPE, PROP_NAME)	\
+  do {								\
+    marshalled_obj->set (#PROP_NAME, PROP_NAME.to_json ());	\
+  } while (0)
+
+// FIXME
+
+#define MARSHAL_OPTIONAL_PROPERTY(PROP_TYPE, PROP_NAME)	\
+  do {								\
+    if (auto val_ptr = PROP_NAME.get_optional_value ())	\
+      marshalled_obj->set (#PROP_NAME, val_ptr->to_json ());	\
+  } while (0)
+
+/* FIXME.  */
+
+#define END_DEFINE_TO_JSON \
+  return marshalled_obj;   \
+ }
+
+
+// TODO: autogenerate the interface binding/marshalling/demarshalling code
+// from an interface description.
+
+/* struct boolean.  */
+
+lsp::result<lsp::spec::boolean>
+lsp::spec::boolean::from_json (const unmarshaller &ctxt,
+				const json::value &jv)
+{
+  gcc_unreachable (); // FIXME
+}
+
+std::unique_ptr<json::value>
+lsp::spec::boolean::to_json () const
+{
+  return std::make_unique<json::literal> (m_val
+					  ? json::JSON_TRUE
+					  : json::JSON_FALSE);
+}
+
+/* struct uinteger.  */
+
+lsp::result<lsp::spec::uinteger>
+lsp::spec::uinteger::from_json (const unmarshaller &ctxt,
+				const json::value &jv)
+{
+  const json::integer_number *js_int = jv.dyn_cast_integer_number ();
+  if (!js_int)
+    return not_an_integer<lsp::spec::uinteger> (ctxt, jv);
+
+  // FIXME: check for negatives etc
+
+  lsp::spec::uinteger unmarshalled_val;
+  unmarshalled_val.m_val = js_int->get ();
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::uinteger::to_json () const
+{
+  return std::make_unique<json::integer_number> (m_val);
+}
+
+/* struct integer.  */
+
+lsp::result<lsp::spec::integer>
+lsp::spec::integer::from_json (const unmarshaller &ctxt,
+			       const json::value &jv)
+{
+  const json::integer_number *js_int = jv.dyn_cast_integer_number ();
+  if (!js_int)
+    return not_an_integer<lsp::spec::integer> (ctxt, jv);
+
+  lsp::spec::integer unmarshalled_val;
+  unmarshalled_val.m_val = js_int->get ();
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::integer::to_json () const
+{
+  return std::make_unique<json::integer_number> (m_val);
+}
+
+// struct string
+
+lsp::result<lsp::spec::string>
+lsp::spec::string::from_json (const unmarshaller &ctxt,
+				   const json::value &jv)
+{
+  const json::string *js_str = jv.dyn_cast_string ();
+  if (!js_str)
+    return not_a_string<string> (ctxt, jv);
+
+  lsp::spec::string unmarshalled_val;
+  unmarshalled_val.m_val = js_str->get_string ();
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::string::to_json () const
+{
+  return std::make_unique<json::string> (m_val.c_str ());
+}
+
+// struct DocumentUri : public string
+
+lsp::result<lsp::spec::DocumentUri>
+lsp::spec::DocumentUri::from_json (const unmarshaller &ctxt,
+				   const json::value &jv)
+{
+  const json::string *js_str = jv.dyn_cast_string ();
+  if (!js_str)
+    return not_a_string<DocumentUri> (ctxt, jv);
+
+  lsp::spec::DocumentUri unmarshalled_val;
+  unmarshalled_val.m_val = js_str->get_string ();
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::DocumentUri::to_json () const
+{
+  return std::make_unique<json::string> (m_val.c_str ());
+}
+
+// struct URI : public string
+
+lsp::result<lsp::spec::URI>
+lsp::spec::URI::from_json (const unmarshaller &ctxt,
+				   const json::value &jv)
+{
+  const json::string *js_str = jv.dyn_cast_string ();
+  if (!js_str)
+    return not_a_string<URI> (ctxt, jv);
+
+  lsp::spec::URI unmarshalled_val;
+  unmarshalled_val.m_val = js_str->get_string ();
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::URI::to_json () const
+{
+  return std::make_unique<json::string> (m_val.c_str ());
+}
+
+// struct ClientCapabilities
+
+BEGIN_DEFINE_FROM_JSON(ClientCapabilities)
+{
+  // FIXME
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(ClientCapabilities)
+{
+  // FIXME
+}
+END_DEFINE_TO_JSON
+
+// struct InitializeParams // FIXME : public WorkDoneProgressParams
+
+lsp::result<lsp::spec::InitializeParams>
+lsp::spec::InitializeParams::from_json (const unmarshaller &ctxt,
+					const json::value &jv)
+{
+  const char *const type_name = "InitializeParams";
+
+  const json::object *obj = jv.dyn_cast_object ();
+  if (!obj)
+    return not_an_object<InitializeParams> (ctxt, type_name, jv);
+
+  lsp::spec::InitializeParams unmarshalled_val;
+  // FIXME: clientInfo
+  UNMARSHAL_REQUIRED_PROPERTY (ClientCapabilities, capabilities);
+
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::InitializeParams::to_json () const
+{
+  gcc_unreachable (); // FIXME
+}
+
+// struct ServerCapabilities
+
+BEGIN_DEFINE_FROM_JSON(ServerCapabilities)
+{
+  // FIXME
+  //UNMARSHAL_OPTIONAL_PROPERTY (boolean, documentSymbolProvider);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(ServerCapabilities)
+{
+  // FIXME
+  MARSHAL_OPTIONAL_PROPERTY (boolean, documentSymbolProvider);
+}
+END_DEFINE_TO_JSON
+
+// struct InitializeResult
+
+BEGIN_DEFINE_FROM_JSON(InitializeResult)
+{
+  gcc_unreachable (); // FIXME
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(InitializeResult)
+{
+  MARSHAL_REQUIRED_PROPERTY (ServerCapabilities, capabilities);
+  MARSHAL_OPTIONAL_PROPERTY (serverInfo_t, serverInfo);
+}
+END_DEFINE_TO_JSON
+
+// struct InitializeResult::serverInfo_t
+
+BEGIN_DEFINE_FROM_JSON(InitializeResult::serverInfo_t)
+{
+  gcc_unreachable (); // FIXME
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(InitializeResult::serverInfo_t)
+{
+  MARSHAL_REQUIRED_PROPERTY (string, name);
+  MARSHAL_OPTIONAL_PROPERTY (string, version);
+}
+END_DEFINE_TO_JSON
+
+// struct InitializedParams
+
+BEGIN_DEFINE_FROM_JSON(InitializedParams)
+{
+  /* empty.  */
+}
+END_DEFINE_FROM_JSON
+
+// struct Position
+
+result<lsp::spec::Position>
+lsp::spec::Position::from_json (const unmarshaller &ctxt,
+				const json::value &jv)
+{
+  const char *const type_name = "Position";
+
+  const json::object *obj = jv.dyn_cast_object ();
+  if (!obj)
+    return not_an_object<Position> (ctxt, type_name, jv);
+
+  lsp::spec::Position unmarshalled_val;
+  UNMARSHAL_REQUIRED_PROPERTY (uinteger, line);
+  UNMARSHAL_REQUIRED_PROPERTY (uinteger, character);
+  return unmarshalled_val;
+}
+
+std::unique_ptr<json::value>
+lsp::spec::Position::to_json () const
+{
+  auto jobj = std::make_unique<json::object> ();
+  jobj->set ("line", line.to_json ());
+  jobj->set ("character", character.to_json ());
+  return jobj;
+}
+
+lsp::spec::Position
+lsp::spec::Position::from_expanded_location (const expanded_location &exp_loc)
+{
+  Position result;
+
+  /* Convert from GCC's 1-based lines and columns to LSP's
+     0-based lines and columns.  */
+  result.line.m_val = exp_loc.line - 1;
+  result.character.m_val = exp_loc.column - 1; // FIXME: what about non-ASCII
+
+  return result;
+}
+
+/* struct Range.  */
+
+BEGIN_DEFINE_TO_JSON(Range)
+{
+  MARSHAL_REQUIRED_PROPERTY (Position, start);
+  MARSHAL_REQUIRED_PROPERTY (Position, end);
+}
+END_DEFINE_TO_JSON
+
+BEGIN_DEFINE_FROM_JSON(Range)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (Position, start);
+  UNMARSHAL_REQUIRED_PROPERTY (Position, end);
+}
+END_DEFINE_FROM_JSON
+
+lsp::spec::Range
+lsp::spec::Range::from_location_t (location_t loc)
+{
+  location_t start = get_start (loc);
+  location_t finish = get_finish (loc);
+  expanded_location exp_loc_start = expand_location (start);
+  expanded_location exp_loc_finish = expand_location (finish);
+
+  /* Offset the end column by 1 since GCC finish locations are inclusive,
+       whereas in LSP the endpoint of a Range is exclusive.  */
+  exp_loc_finish.column += 1;
+
+  Range result;
+  result.start = Position::from_expanded_location (exp_loc_start);
+  result.end = Position::from_expanded_location (exp_loc_finish);
+  return result;
+}
+
+/* struct Location.  */
+
+BEGIN_DEFINE_TO_JSON(Location)
+{
+  MARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  MARSHAL_REQUIRED_PROPERTY (Range, range);
+}
+END_DEFINE_TO_JSON
+
+BEGIN_DEFINE_FROM_JSON(Location)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  UNMARSHAL_REQUIRED_PROPERTY (Range, range);
+}
+END_DEFINE_FROM_JSON
+
+lsp::spec::Location
+lsp::spec::Location::from_location_t (location_t loc)
+{
+  Location result;
+  expanded_location exp_loc = expand_location (loc);
+  if (exp_loc.file)
+    {
+      uris::file_uri file_uri (exp_loc.file);
+      result.uri.m_val = file_uri.to_string ();
+    }
+  result.range = lsp::spec::Range::from_location_t (loc);
+  return result;
+}
+
+// struct TextDocumentIdentifier
+
+BEGIN_DEFINE_FROM_JSON(TextDocumentIdentifier)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(TextDocumentIdentifier)
+{
+  MARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+}
+END_DEFINE_TO_JSON
+
+// struct VersionedTextDocumentIdentifier : public TextDocumentIdentifier
+
+BEGIN_DEFINE_FROM_JSON(VersionedTextDocumentIdentifier)
+{
+  // FIXME: how to share impl with base class?
+  UNMARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  UNMARSHAL_REQUIRED_PROPERTY (integer, version);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(VersionedTextDocumentIdentifier)
+{
+  // FIXME: how to share impl with base class?
+  MARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  MARSHAL_REQUIRED_PROPERTY (integer, version);
+}
+END_DEFINE_TO_JSON
+
+// struct TextDocumentItem
+
+BEGIN_DEFINE_FROM_JSON(TextDocumentItem)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  UNMARSHAL_REQUIRED_PROPERTY (string, languageId);
+  UNMARSHAL_REQUIRED_PROPERTY (integer, version);
+  UNMARSHAL_REQUIRED_PROPERTY (string, text);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(TextDocumentItem)
+{
+  MARSHAL_REQUIRED_PROPERTY (DocumentUri, uri);
+  MARSHAL_REQUIRED_PROPERTY (string, languageId);
+  MARSHAL_REQUIRED_PROPERTY (integer, version);
+  MARSHAL_REQUIRED_PROPERTY (string, text);
+}
+END_DEFINE_TO_JSON
+
+// struct DidOpenTextDocumentParams
+
+BEGIN_DEFINE_FROM_JSON(DidOpenTextDocumentParams)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (TextDocumentItem, textDocument);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DidOpenTextDocumentParams)
+{
+  MARSHAL_REQUIRED_PROPERTY (TextDocumentItem, textDocument);
+}
+END_DEFINE_TO_JSON
+
+// struct TextDocumentContentChangeEvent
+
+BEGIN_DEFINE_FROM_JSON(TextDocumentContentChangeEvent)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (string, text);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(TextDocumentContentChangeEvent)
+{
+  MARSHAL_REQUIRED_PROPERTY (string, text);
+}
+END_DEFINE_TO_JSON
+
+// struct DidChangeTextDocumentParams
+
+BEGIN_DEFINE_FROM_JSON(DidChangeTextDocumentParams)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (VersionedTextDocumentIdentifier, textDocument);
+  UNMARSHAL_REQUIRED_PROPERTY (vector<TextDocumentContentChangeEvent>, contentChanges);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DidChangeTextDocumentParams)
+{
+  MARSHAL_REQUIRED_PROPERTY (VersionedTextDocumentIdentifier, textDocument);
+  MARSHAL_REQUIRED_PROPERTY (vector<TextDocumentContentChangeEvent>, contentChanges);
+}
+END_DEFINE_TO_JSON
+
+// struct DidCloseTextDocumentParams
+
+BEGIN_DEFINE_FROM_JSON(DidCloseTextDocumentParams)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DidCloseTextDocumentParams)
+{
+  MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument);
+}
+END_DEFINE_TO_JSON
+
+// struct TextDocumentPositionParams
+
+lsp::result<lsp::spec::TextDocumentPositionParams>
+lsp::spec::TextDocumentPositionParams::from_json (const unmarshaller &ctxt,
+						  const json::value &jv)
+{
+  TextDocumentPositionParams unmarshalled_val;
+  const char *const type_name = "TextDocumentPositionParams";
+
+  const json::object *obj = jv.dyn_cast_object ();
+  if (!obj)
+    return not_an_object<TextDocumentPositionParams> (ctxt, type_name, jv);
+
+  UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument);
+  UNMARSHAL_REQUIRED_PROPERTY (Position, position);
+
+  return unmarshalled_val;
+}
+
+// struct DiagnosticSeverity
+
+std::unique_ptr<json::value>
+lsp::spec::DiagnosticSeverity::to_json () const
+{
+  return std::make_unique<json::integer_number> (m_val);
+}
+
+// struct DiagnosticRelatedInformation
+
+BEGIN_DEFINE_FROM_JSON(DiagnosticRelatedInformation)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (Location, location);
+  UNMARSHAL_REQUIRED_PROPERTY (string, message);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DiagnosticRelatedInformation)
+{
+  MARSHAL_REQUIRED_PROPERTY (Location, location);
+  MARSHAL_REQUIRED_PROPERTY (string, message);
+}
+END_DEFINE_TO_JSON
+
+// struct CodeDescription
+
+BEGIN_DEFINE_FROM_JSON(CodeDescription)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (URI, href);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(CodeDescription)
+{
+  MARSHAL_REQUIRED_PROPERTY (URI, href);
+}
+END_DEFINE_TO_JSON
+
+// struct Diagnostic
+
+std::unique_ptr<json::value>
+lsp::spec::Diagnostic::to_json () const
+{
+  auto jobj = std::make_unique<json::object> ();
+  jobj->set ("range", range.to_json ());
+  if (auto val = severity.get_optional_value ())
+    jobj->set ("severity", val->to_json ());
+  if (auto val = code.get_optional_value ())
+    jobj->set ("code", val->to_json ());
+  if (auto val = codeDescription.get_optional_value ())
+    jobj->set ("codeDescription", val->to_json ());
+
+  jobj->set ("message", message.to_json ());
+
+  if (auto val = relatedInformation.get_optional_value ())
+    jobj->set ("relatedInformation", val->to_json ());
+
+  return jobj;
+}
+
+// struct PublishDiagnosticsParams
+
+std::unique_ptr<json::value>
+lsp::spec::PublishDiagnosticsParams::to_json () const
+{
+  auto jobj = std::make_unique<json::object> ();
+  jobj->set ("uri", uri.to_json ());
+  if (auto val = version.get_optional_value ())
+    jobj->set ("version", val->to_json ());
+  jobj->set ("diagnostics", diagnostics.to_json ());
+
+  return jobj;
+}
+
+// struct DocumentSymbolParams
+
+BEGIN_DEFINE_FROM_JSON(DocumentSymbolParams)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DocumentSymbolParams)
+{
+  MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument);
+}
+END_DEFINE_TO_JSON
+
+// struct SymbolKind
+
+BEGIN_DEFINE_FROM_JSON(SymbolKind)
+{
+  gcc_unreachable (); // FIXME
+}
+END_DEFINE_FROM_JSON
+
+std::unique_ptr<json::value>
+lsp::spec::SymbolKind::to_json () const
+{
+  return std::make_unique<json::integer_number> (m_val);
+}
+
+// struct DocumentSymbol
+
+BEGIN_DEFINE_FROM_JSON(DocumentSymbol)
+{
+  UNMARSHAL_REQUIRED_PROPERTY (string, name);
+  UNMARSHAL_OPTIONAL_PROPERTY (string, detail);
+  UNMARSHAL_REQUIRED_PROPERTY (SymbolKind, kind);
+  UNMARSHAL_REQUIRED_PROPERTY (Range, range);
+  UNMARSHAL_REQUIRED_PROPERTY (Range, selectionRange);
+  UNMARSHAL_OPTIONAL_PROPERTY (vector<DocumentSymbol>, children);
+}
+END_DEFINE_FROM_JSON
+
+BEGIN_DEFINE_TO_JSON(DocumentSymbol)
+{
+  MARSHAL_REQUIRED_PROPERTY (string, name);
+  MARSHAL_OPTIONAL_PROPERTY (string, detail);
+  MARSHAL_REQUIRED_PROPERTY (SymbolKind, kind);
+  MARSHAL_REQUIRED_PROPERTY (Range, range);
+  MARSHAL_REQUIRED_PROPERTY (Range, selectionRange);
+  MARSHAL_OPTIONAL_PROPERTY (vector<DocumentSymbol>, children);
+}
+END_DEFINE_TO_JSON
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Selftests.  */
+
+static void
+test_unmarshal_uinteger_not_an_int ()
+{
+  json::array arr;
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::uinteger::from_json (ctxt, arr);
+  auto err = r.get_error ();
+  ASSERT_NE (err, nullptr);
+  ASSERT_EQ (err->get_code (), json_rpc::INVALID_PARAMS);
+  ASSERT_STREQ (err->get_message (), "expected integer for ''; got array");
+  ASSERT_EQ (err->get_data (), nullptr);
+}
+
+static void
+test_unmarshal_uinteger_ok ()
+{
+  json::integer_number num (42);
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::uinteger::from_json (ctxt, num);
+  auto err = r.get_error ();
+  ASSERT_EQ (err, nullptr);
+  ASSERT_EQ (r.take_value ().m_val, 42);
+}
+
+static void
+test_unmarshal_integer_not_an_int ()
+{
+  json::array arr;
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::integer::from_json (ctxt, arr);
+  auto err = r.get_error ();
+  ASSERT_NE (err, nullptr);
+  ASSERT_EQ (err->get_code (), json_rpc::INVALID_PARAMS);
+  ASSERT_STREQ (err->get_message (), "expected integer for ''; got array");
+  ASSERT_EQ (err->get_data (), nullptr);
+}
+
+static void
+test_unmarshal_integer_ok ()
+{
+  json::integer_number num (-9);
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::uinteger::from_json (ctxt, num);
+  auto err = r.get_error ();
+  ASSERT_EQ (err, nullptr);
+  ASSERT_EQ (r.take_value ().m_val, -9);
+}
+
+static void
+test_unmarshal_DocumentUri_not_a_string ()
+{
+  json::array arr;
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::DocumentUri::from_json (ctxt, arr);
+  auto err = r.get_error ();
+  ASSERT_NE (err, nullptr);
+  ASSERT_EQ (err->get_code (), json_rpc::INVALID_PARAMS);
+  ASSERT_STREQ (err->get_message (), "expected string for ''; got array");
+  ASSERT_EQ (err->get_data (), nullptr);
+}
+
+static void
+test_unmarshal_DocumentUri_ok ()
+{
+  json::string str ("foo.c");
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::DocumentUri::from_json (ctxt, str);
+  auto err = r.get_error ();
+  ASSERT_EQ (err, nullptr);
+  ASSERT_STREQ (r.take_value ().m_val.c_str (), "foo.c");
+}
+
+static void
+test_unmarshal_Position_missing_params ()
+{
+  json::object js_obj;
+
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::Position::from_json (ctxt, js_obj);
+  auto err = r.get_error ();
+  ASSERT_EQ (err->get_code (), json_rpc::INVALID_PARAMS);
+  ASSERT_STREQ (err->get_message (),
+		"missing 'line' property for Position object ''");
+  ASSERT_EQ (err->get_data (), nullptr);
+}
+
+static void
+test_unmarshal_TextDocumentPositionParams_ok ()
+{
+  json::object js_obj;
+
+  auto js_textDocument = std::make_unique<json::object> ();
+  js_textDocument->set_string ("uri", "foo.c");
+  js_obj.set ("textDocument", std::move (js_textDocument));
+
+  auto js_position = std::make_unique<json::object> ();
+  js_position->set_integer ("line", 42);
+  js_position->set_integer ("character", 23);
+  js_obj.set ("position", std::move (js_position));
+
+  const unmarshaller ctxt (nullptr);
+  auto r = spec::TextDocumentPositionParams::from_json (ctxt, js_obj);
+  auto err = r.get_error ();
+  ASSERT_EQ (err, nullptr);
+  auto unmarshalled_val = r.take_value ();
+
+  ASSERT_STREQ (unmarshalled_val.textDocument.uri.m_val.c_str (), "foo.c");
+  ASSERT_EQ (unmarshalled_val.position.line.m_val, 42);
+  ASSERT_EQ (unmarshalled_val.position.character.m_val, 23);
+}
+
+
+/* Run all of the selftests within this file.  */
+
+void
+lsp_spec_cc_tests ()
+{
+  test_unmarshal_uinteger_not_an_int ();
+  // FIXME: negatives
+  test_unmarshal_uinteger_ok ();
+  test_unmarshal_integer_not_an_int ();
+  test_unmarshal_integer_ok ();
+  test_unmarshal_DocumentUri_not_a_string ();
+  test_unmarshal_DocumentUri_ok ();
+  test_unmarshal_Position_missing_params ();
+#if 0
+  test_unmarshal_TextDocumentPositionParams_error ();
+#endif
+  test_unmarshal_TextDocumentPositionParams_ok ();
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/lsp/lsp-spec.h b/gcc/lsp/lsp-spec.h
new file mode 100644
index 00000000000..efc4fdc7138
--- /dev/null
+++ b/gcc/lsp/lsp-spec.h
@@ -0,0 +1,591 @@
+/* Language Server Protocol implementation.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_SPEC_H
+#define GCC_LSP_SPEC_H
+
+#include "lsp/json-rpc.h"
+#include "pretty-print.h"
+#include "pretty-print-markup-json.h"
+
+namespace lsp {
+
+/* Class for the result of an operation: either a value or an error
+   The ValueType must be default-constructible.  */
+
+template <typename ValueType>
+class result
+{
+ public:
+  result (ValueType &&value)
+  : m_value (std::move (value)),
+    m_error ()
+  {}
+
+  result (std::unique_ptr<json_rpc::error> error)
+  : m_value (),
+    m_error (std::move (error))
+  {}
+
+  ValueType
+  take_value ()
+  {
+    return std::move (m_value);
+  }
+
+  const json_rpc::error *
+  get_error ()
+  {
+    return m_error.get ();
+  }
+
+  std::unique_ptr<json_rpc::error>
+  take_error ()
+  {
+    return std::move (m_error);
+  }
+
+  ValueType m_value;
+  std::unique_ptr<json_rpc::error> m_error;
+};
+
+/* FIXME.  */
+
+template<>
+class result<void>
+{
+ public:
+  result () = default;
+
+  result (std::unique_ptr<json_rpc::error> error)
+  : m_error (std::move (error))
+  {}
+
+  const json_rpc::error *
+  get_error ()
+  {
+    return m_error.get ();
+  }
+
+  std::unique_ptr<json_rpc::error>
+  take_error ()
+  {
+    return std::move (m_error);
+  }
+
+  std::unique_ptr<json_rpc::error> m_error;
+};
+
+/* Context info for implementing from_json functions.  */
+
+class unmarshaller
+{
+public:
+  unmarshaller (const json::value *req_id)
+  : m_req_id (req_id)
+  {
+  }
+
+  template <typename ValueType>
+  result<ValueType>
+  invalid_params (const char *msg) const
+  {
+    return result<ValueType>
+      (std::make_unique<json_rpc::error>
+       (json_rpc::error::invalid_params (make_req_id (), msg)));
+  }
+
+  static const char *
+  describe_kind (const json::value &jv);
+
+  std::unique_ptr<json::value>
+  make_req_id () const
+  {
+    if (!m_req_id)
+      return nullptr;
+    return m_req_id->clone ();
+  }
+
+private:
+  const json::value *m_req_id;
+};
+
+template <typename Type>
+class optional
+{
+public:
+  optional () = default;
+
+  optional (Type &&t)
+  : m_ptr (std::make_unique<Type> (std::move (t)))
+  {}
+
+  Type *
+  get_optional_value () { return m_ptr.get (); }
+
+  const Type *
+  get_optional_value () const { return m_ptr.get (); }
+
+  void
+  set_optional_value (Type &&t)
+  {
+    m_ptr = std::make_unique<Type> (std::move (t));
+  }
+
+private:
+  std::unique_ptr<Type> m_ptr;
+};
+
+template <typename ValueType>
+result<ValueType>
+not_an_array (const unmarshaller &ctxt,
+	      const json::value &jv)
+{
+  pretty_printer pp;
+  pp_markup::quoted_json_pointer e_js_pointer (jv);
+  pp_printf (&pp, "expected array for %e; got %s",
+	     &e_js_pointer, ctxt.describe_kind (jv));
+  return ctxt.invalid_params<ValueType> (pp_formatted_text (&pp));
+}
+
+template <typename ElementType>
+class vector : public std::vector<ElementType>
+{
+public:
+  static result<vector<ElementType>>
+  from_json (const unmarshaller &ctxt,
+	     const json::value &jv)
+  {
+    const json::array *js_arr = jv.dyn_cast_array ();
+    if (!js_arr)
+      return not_an_array<vector<ElementType>> (ctxt, jv);
+    vector<ElementType> v;
+    for (size_t i = 0; i < js_arr->size (); ++i)
+      {
+	result<ElementType> element_res
+	  = ElementType::from_json (ctxt, *(*js_arr)[i]);
+	if (element_res.get_error ())
+	  return element_res.take_error ();
+	v.push_back (element_res.take_value ());
+      }
+    return v;
+  }
+
+  std::unique_ptr<json::value>
+  to_json () const
+  {
+    auto json_arr = std::make_unique<json::array> ();
+    for (auto &iter : *this)
+      json_arr->append (iter.to_json ());
+    return json_arr;
+  }
+};
+
+namespace spec {
+
+#define DECLARE_JSON_METHODS(TYPENAME) \
+  static result<TYPENAME>	       \
+  from_json (const unmarshaller &ctxt, \
+	     const json::value &jv);   \
+				       \
+  std::unique_ptr<json::value>	       \
+  to_json () const;
+
+/* Types from the LSP protocol specification.  This uses camel case,
+   and so we do here, within this namespace, at least.  */
+
+// TODO: autogenerate the interface binding/marshalling/demarshalling code
+// from an interface description.
+
+struct boolean
+{
+  DECLARE_JSON_METHODS(boolean)
+
+  boolean () : m_val (false) {}
+  boolean (bool val) : m_val (val) {}
+
+  bool m_val;
+};
+
+struct uinteger
+{
+  DECLARE_JSON_METHODS(uinteger)
+
+  uinteger () : m_val (0) {}
+
+  unsigned m_val;
+};
+
+struct integer
+{
+  DECLARE_JSON_METHODS(integer)
+
+  integer () : m_val (0) {}
+
+  bool
+  operator< (const integer &other) const
+  {
+    return m_val < other.m_val;
+  }
+
+  int m_val;
+};
+
+struct string
+{
+  DECLARE_JSON_METHODS(string)
+
+  string () = default;
+  string (const char *s) : m_val (s) {}
+
+  std::string m_val;
+};
+
+struct DocumentUri : public string
+{
+  DECLARE_JSON_METHODS(DocumentUri)
+
+  bool
+  operator< (const DocumentUri &other) const
+  {
+    return m_val < other.m_val;
+  }
+};
+
+struct URI : public string
+{
+  DECLARE_JSON_METHODS(URI)
+
+  bool
+  operator< (const URI &other) const
+  {
+    return m_val < other.m_val;
+  }
+};
+
+struct ClientCapabilities
+{
+  DECLARE_JSON_METHODS(ClientCapabilities)
+
+  // FIXME
+};
+
+struct InitializeParams // FIXME : public WorkDoneProgressParams
+{
+  DECLARE_JSON_METHODS(InitializeParams);
+
+  // FIXME:
+  //optional<int> m_processId;
+
+  struct clientInfo {
+    string name;
+    optional<string> version;
+  };
+  optional<clientInfo> clientInfo;
+
+  // FIXME
+
+  ClientCapabilities capabilities;
+#if 0
+  optional<TraceValue> trace;
+#endif
+
+  // FIXME
+};
+
+struct ServerCapabilities
+{
+  DECLARE_JSON_METHODS(ServerCapabilities)
+
+  // FIXME:
+
+  optional<boolean> documentSymbolProvider;
+};
+
+struct InitializeResult
+{
+  DECLARE_JSON_METHODS(InitializeResult);
+
+  ServerCapabilities capabilities;
+
+  struct serverInfo_t
+  {
+    DECLARE_JSON_METHODS(InitializeResult::serverInfo_t);
+    string name;
+    optional<string> version;
+  };
+  optional<serverInfo_t> serverInfo;
+};
+
+struct InitializedParams
+{
+  DECLARE_JSON_METHODS(InitializedParams);
+};
+
+/* Note that LSP uses 0-based lines and characters, whereas GCC uses
+   1-based lines and columns.  */
+
+struct Position
+{
+  DECLARE_JSON_METHODS(Position)
+
+  static Position
+  from_expanded_location (const expanded_location &);
+
+  uinteger line;
+  uinteger character;
+};
+
+struct Range
+{
+  DECLARE_JSON_METHODS(Range)
+
+  static Range
+  from_location_t (location_t);
+
+  Position start;
+  Position end;
+};
+
+struct Location
+{
+  DECLARE_JSON_METHODS(Location)
+
+  static Location
+  from_location_t (location_t);
+
+  DocumentUri uri;
+  Range range;
+};
+
+struct TextDocumentIdentifier
+{
+  DECLARE_JSON_METHODS(TextDocumentIdentifier)
+
+  DocumentUri uri;
+};
+
+struct VersionedTextDocumentIdentifier : public TextDocumentIdentifier
+{
+  DECLARE_JSON_METHODS(VersionedTextDocumentIdentifier)
+
+  integer version;
+};
+
+struct TextDocumentItem
+{
+  DECLARE_JSON_METHODS(TextDocumentItem)
+
+  DocumentUri uri;
+  string languageId;
+  integer version;
+  string text;
+};
+
+struct DidOpenTextDocumentParams
+{
+  DECLARE_JSON_METHODS(DidOpenTextDocumentParams)
+
+  TextDocumentItem textDocument;
+};
+
+struct TextDocumentContentChangeEvent
+{
+  DECLARE_JSON_METHODS(TextDocumentContentChangeEvent)
+
+#if 0
+  Range range;
+  optional<uinteger> rangeLength;
+  string text;
+#else
+  string text;
+#endif
+};
+
+struct DidChangeTextDocumentParams
+{
+  DECLARE_JSON_METHODS(DidChangeTextDocumentParams)
+
+  VersionedTextDocumentIdentifier textDocument;
+  vector<TextDocumentContentChangeEvent> contentChanges;
+};
+
+struct DidCloseTextDocumentParams
+{
+  DECLARE_JSON_METHODS(DidCloseTextDocumentParams)
+
+  TextDocumentIdentifier textDocument;
+};
+
+struct TextDocumentPositionParams
+{
+  DECLARE_JSON_METHODS(TextDocumentPositionParams)
+
+  TextDocumentIdentifier textDocument;
+  Position position;
+};
+
+struct DiagnosticSeverity
+{
+  DECLARE_JSON_METHODS(DiagnosticSeverity)
+  enum  {
+    Error = 1,
+    Warning = 2,
+    Information = 3,
+    Hint = 4
+  } m_val;
+};
+
+struct DiagnosticRelatedInformation
+{
+  DECLARE_JSON_METHODS(DiagnosticRelatedInformation)
+  Location location;
+  string message;
+};
+
+struct CodeDescription
+{
+  DECLARE_JSON_METHODS(CodeDescription)
+  URI href;
+};
+
+struct Diagnostic
+{
+  DECLARE_JSON_METHODS(Diagnostic)
+
+  Range range;
+  optional<DiagnosticSeverity> severity;
+  optional<string> code; // FIXME: can also be integer in the spec
+  optional<CodeDescription> codeDescription;
+
+#if 0
+	/**
+	 * A human-readable string describing the source of this
+	 * diagnostic, e.g. 'typescript' or 'super lint'.
+	 */
+	source?: string;
+#endif
+
+  string message;
+
+#if 0
+	/**
+	 * Additional metadata about the diagnostic.
+	 *
+	 * @since 3.15.0
+	 */
+	tags?: DiagnosticTag[];
+#endif
+
+  optional<vector<DiagnosticRelatedInformation>> relatedInformation;
+
+#if 0
+	/**
+	 * A data entry field that is preserved between a
+	 * `textDocument/publishDiagnostics` notification and
+	 * `textDocument/codeAction` request.
+	 *
+	 * @since 3.16.0
+	 */
+	data?: LSPAny;
+#endif
+};
+
+struct PublishDiagnosticsParams
+{
+  DECLARE_JSON_METHODS(PublishDiagnosticsParams)
+
+  DocumentUri uri;
+  optional<integer> version;
+  vector<Diagnostic> diagnostics;
+};
+
+struct DocumentSymbolParams // FIXME
+{
+  DECLARE_JSON_METHODS(DocumentSymbolParams)
+
+  TextDocumentIdentifier textDocument;
+};
+
+struct SymbolKind
+{
+  DECLARE_JSON_METHODS(SymbolKind)
+  enum {
+    File = 1,
+    Module = 2,
+    Namespace = 3,
+    Package = 4,
+    Class = 5,
+    Method = 6,
+    Property = 7,
+    Field = 8,
+    Constructor = 9,
+    Enum = 10,
+    Interface = 11,
+    Function = 12,
+    Variable = 13,
+    Constant = 14,
+    String = 15,
+    Number = 16,
+    Boolean = 17,
+    Array = 18,
+    Object = 19,
+    Key = 20,
+    Null = 21,
+    EnumMember = 22,
+    Struct = 23,
+    Event = 24,
+    Operator = 25,
+    TypeParameter = 26
+  } m_val;
+};
+
+struct DocumentSymbol
+{
+  DECLARE_JSON_METHODS(DocumentSymbol)
+
+  string name;
+  optional<string> detail;
+  SymbolKind kind;
+
+#if 0
+	/**
+	 * Tags for this document symbol.
+	 *
+	 * @since 3.16.0
+	 */
+	tags?: SymbolTag[];
+
+	/**
+	 * Indicates if this symbol is deprecated.
+	 *
+	 * @deprecated Use tags instead
+	 */
+	deprecated?: boolean;
+#endif
+  Range range;
+  Range selectionRange;
+  optional<vector<DocumentSymbol>> children;
+};
+
+} // namespace lsp::spec
+} // namespace lsp
+
+#endif  /* GCC_LSP_SPEC_H  */
diff --git a/gcc/lsp/lsp-to-octets.h b/gcc/lsp/lsp-to-octets.h
new file mode 100644
index 00000000000..8651747f673
--- /dev/null
+++ b/gcc/lsp/lsp-to-octets.h
@@ -0,0 +1,59 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_LSP_TO_OCTETS_H
+#define GCC_LSP_LSP_TO_OCTETS_H
+
+#include "lsp/octets.h"
+#include "lsp/mini-http.h"
+#include "lsp/json-rpc.h"
+
+// FIXME:
+
+class lsp_to_octets : public lsp::logger
+{
+public:
+  lsp_to_octets ()
+  : m_json_rpc_sink (nullptr),
+    m_malformed_json_sink (nullptr),
+    m_invalid_json_sink (nullptr),
+    m_endpoint (nullptr)
+  {}
+
+  void
+  set_json_rpc_endpoint (json_rpc::endpoint *endpoint)
+  {
+    m_endpoint = endpoint;
+  }
+  
+  void on_consume_octets (octets::buffer data) final override;
+  void on_end_of_octet_stream () final override;
+
+private:
+  void consume_http_message (std::unique_ptr<mini_http::message> http_msg);
+
+  mini_http::message_parser m_http_parser;
+  json_rpc::sink *m_json_rpc_sink; // FIXME
+  json_messaging::malformed_json_sink *m_malformed_json_sink; // FIXME
+  json_rpc::invalid_json_sink *m_invalid_json_sink; // FIXME
+  json_rpc::endpoint *m_endpoint;
+};
+
+#endif  /* GCC_LSP_LSP_TO_OCTETS_H  */
diff --git a/gcc/lsp/mini-http.cc b/gcc/lsp/mini-http.cc
new file mode 100644
index 00000000000..52ae0d74169
--- /dev/null
+++ b/gcc/lsp/mini-http.cc
@@ -0,0 +1,389 @@
+/* Implementation of the mini-HTTP-like protocol used by LSP.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_ALGORITHM
+#define INCLUDE_LIST
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "lsp/mini-http.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "diagnostic.h"
+
+// class mini_http::message
+
+octets::buffer
+mini_http::message::make_octets_buffer () const
+{
+  pretty_printer pp;
+  pp_printf (&pp, "Content-Length: %li\r\n",
+	     (long)get_content_length ());
+  pp_string (&pp, "\r\n");
+
+  octets::buffer buf;
+  for (const char *ch = pp_formatted_text (&pp); *ch; ++ch)
+    buf.push_back (*ch);
+  buf.insert (buf.end (),
+	      get_content ().begin (),
+	      get_content ().end ());
+  return buf;
+}
+
+void
+mini_http::message::send_to_ipc_sender (ipc::sender &sender) const
+{
+  octets::buffer buf (make_octets_buffer ());
+  sender.add_data_to_queue (std::move (buf));
+}
+
+// class mini_http::message_parser : public lsp::log_user
+
+mini_http::message_parser::message_parser ()
+: m_phase (phase::parsing_headers),
+  m_pending_data (),
+  m_content_length (-1)
+{
+}
+
+std::vector<mini_http::message>
+mini_http::message_parser::parse_buffer (const char *buf)
+{
+  std::vector<mini_http::message> msgs;
+  while (*buf)
+    if (auto msg = consume_octet (*(buf++)))
+      msgs.push_back (std::move (*msg));
+  return msgs;
+}
+
+std::unique_ptr<mini_http::message>
+mini_http::message_parser::consume_octet (uint8_t octet)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  switch (m_phase)
+    {
+    default:
+      gcc_unreachable ();
+    case phase::parsing_headers:
+      consume_header_octet (octet);
+      return nullptr;
+    case phase::parsing_body:
+      return consume_body_octet (octet);
+    }
+}
+
+void
+mini_http::message_parser::consume_header_octet (uint8_t octet)
+{
+  m_pending_data.push_back (octet);
+
+  /* Detect "\r\n".  */
+  size_t len = m_pending_data.size ();
+  if (len >= 2 && m_pending_data[len - 2] == '\r' && octet == '\n')
+    {
+      /* Is this a blank line?  If so, then we're done with headers.  */
+      if (len == 2)
+	{
+	  m_phase = phase::parsing_body;
+	  m_pending_data.clear ();
+	}
+      else
+	{
+	  /* Otherwise we have a header.  */
+	  parse_header ();
+	  m_pending_data.clear ();
+	}
+    }
+}
+
+/* FIXME.  */
+
+void
+mini_http::message_parser::parse_header ()
+{
+  /* header-field = field-name ":" OWS field-value OWS  */
+  auto colon = std::find (m_pending_data.begin (),
+			  m_pending_data.end (),
+			  ':');
+  if (colon == m_pending_data.end ())
+    {
+      // Error handling?
+      gcc_unreachable ();
+      return;
+    }
+
+  // The field-name is [m_pending_data.begin (), colon)
+
+  auto start_of_value
+    = skip_optional_whitespace (colon + 1, m_pending_data.end ());
+
+  if (start_of_value  == m_pending_data.end ())
+    {
+      // Error handling?
+      gcc_unreachable ();
+      return;
+    }
+
+  // FIXME: skip trailing whitespace after value
+  auto end_of_value = m_pending_data.end ();
+
+  on_header (m_pending_data.begin (), colon,
+	     start_of_value, end_of_value);
+}
+
+bool
+mini_http::message_parser::is_whitespace_p (uint8_t octet)
+{
+  return octet == ' ' || octet == '\t';
+}
+
+mini_http::message_parser::buffer_iterator
+mini_http::message_parser::
+skip_optional_whitespace (buffer_iterator start,
+			  buffer_iterator end)
+{
+  return std::find_if (start, end,
+		       [](auto octet)
+		       {
+			 return !is_whitespace_p (octet);
+		       });
+}
+
+void
+mini_http::message_parser::on_header (buffer_iterator name_start,
+				      buffer_iterator name_end,
+				      buffer_iterator value_start,
+				      buffer_iterator value_end)
+{
+  if (is_specific_header_p (name_start, name_end, "Content-Length"))
+    {
+      m_content_length = std::stoul (std::string (value_start, value_end));
+      // FIXME: error handling?
+    }
+}
+
+bool
+mini_http::message_parser::
+is_specific_header_p (buffer_iterator name_start,
+		      buffer_iterator name_end,
+		      const char *match_name)
+{
+  size_t match_len = strlen (match_name);
+  if (name_end - name_start != match_len)
+    return false;
+  return 0 == memcmp (&name_start[0], match_name, match_len);
+}
+
+
+std::unique_ptr<mini_http::message>
+mini_http::message_parser::consume_body_octet (uint8_t octet)
+{
+  /* Accumulate data until we've seen Content-Length octets.  */
+  gcc_assert (m_content_length > 0);
+
+  m_pending_data.push_back (octet);
+  if (m_pending_data.size () == m_content_length)
+    {
+      /* We've finished parsing this message.  */
+      auto msg = std::make_unique<message> (std::move (m_pending_data));
+      m_phase = phase::parsing_headers;
+      m_content_length = -1;
+      return msg;
+    }
+
+  return nullptr;
+}
+
+// class octets_to_http : public octets::sink
+
+void
+octets_to_http::on_consume_octets (octets::buffer data)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_http_sink);
+
+  for (auto octet : data)
+    if (auto msg = m_parser.consume_octet (octet))
+      m_http_sink->on_consume_http_message (*msg);
+}
+
+void
+octets_to_http::on_end_of_octet_stream ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_http_sink); 
+
+  m_http_sink->on_end_of_http_stream ();
+}
+
+// class http_to_octets : public mini_http::sink
+
+void
+http_to_octets::on_consume_http_message (const mini_http::message &msg)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_octet_sink);
+
+  octets::buffer buf = msg.make_octets_buffer ();
+  m_octet_sink->on_consume_octets (std::move (buf));
+}
+
+void
+http_to_octets::on_end_of_http_stream ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (m_octet_sink);
+
+  m_octet_sink->on_end_of_octet_stream ();
+}
+
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Selftests.  */
+
+/* Implementation detail of ASSERT_MESSAGE_EQ.  */
+
+static void
+assert_message_eq (const location &loc,
+		   const mini_http::message &msg,
+		   const char *expected_content)
+{
+  size_t expected_len = strlen (expected_content);
+  ASSERT_EQ_AT (loc, msg.get_content_length (), expected_len);
+  octets::selftest::assert_buffer_eq_at (loc, msg.get_content (),
+					 expected_content);
+}
+
+// FIXME
+
+#define ASSERT_MESSAGE_EQ(MSG, EXPECTED_CONTENT) \
+  assert_message_eq ((SELFTEST_LOCATION), (MSG), (EXPECTED_CONTENT))
+
+/* Verify that we can parse an HTTP request.  */
+
+static void
+test_parse_request ()
+{
+  const char *in = ("Host: localhost:4000\r\n"
+		    "Content-Length: 12\r\n"
+		    "content-type: application/json\r\n"
+		    "Accept-Encoding: gzip, deflate, compress\r\n"
+		    "Accept: */*\r\n"
+		    "User-Agent: test-user-agent\r\n"
+		    "\r\n"
+		    "test-content");
+  mini_http::message_parser p;
+  auto msgs = p.parse_buffer (in);
+  ASSERT_EQ (msgs.size (), 1);
+  ASSERT_MESSAGE_EQ (msgs[0], "test-content");
+}
+
+/* Verify that we can split up the parsing of a request at arbitrary
+   places.  */
+
+static void
+test_parse_split_request ()
+{
+  const char *in = ("Host: localhost:4000\r\n"
+		    "Content-Length: 12\r\n"
+		    "content-type: application/json\r\n"
+		    "Accept-Encoding: gzip, deflate, compress\r\n"
+		    "Accept: */*\r\n"
+		    "User-Agent: test-user-agent\r\n"
+		    "\r\n"
+		    "test-content");
+  for (size_t split = 0; split < strlen (in); split++)
+    {
+      std::string s1 (in, in + split);
+      std::string s2 (in + split, in + strlen (in));
+      mini_http::message_parser p;
+      std::vector<mini_http::message> msgs;
+      for (auto msg : p.parse_buffer (s1.c_str ()))
+	msgs.push_back (msg);
+      for (auto msg : p.parse_buffer (s2.c_str ()))
+	msgs.push_back (msg);
+      ASSERT_EQ (msgs.size (), 1);
+      ASSERT_MESSAGE_EQ (msgs[0], "test-content");
+    }
+}
+
+/* Verify that we can parse multiple requests out of one buffer,
+   honoring the Content-Length headers.  */
+
+static void
+test_parse_multiple_requests ()
+{
+  const char *in = ("Content-Length: 25\r\n"
+		    "\r\n"
+		    "This is the first message"
+		    "Content-Length: 26\r\n"
+		    "\r\n"
+		    "This is the second message");
+  mini_http::message_parser p;
+  auto msgs = p.parse_buffer (in);
+  ASSERT_EQ (msgs.size (), 2);
+  ASSERT_MESSAGE_EQ (msgs[0], "This is the first message");
+  ASSERT_MESSAGE_EQ (msgs[1], "This is the second message");
+}
+
+/* Verify http::response::to_str.  */
+
+static void
+test_emit_response ()
+{
+  const char *content = "hello world";
+  // FIXME:
+#if 0
+  mini_http::message msg (content, content + strlen (content));
+  octets::selftest::test_sink octets_sink;
+  http_to_octets http_sink;
+
+  http_sink.on_consume_message (std::move (msg));
+
+  octets_sink.assert_eq_at (SELFTEST_LOCATION,
+			    "Content-Length: 11\r\n"
+			    "\r\n"
+			    "hello world");
+#endif
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+lsp_http_server_cc_tests ()
+{
+  test_parse_request ();
+  test_parse_split_request ();
+  test_parse_multiple_requests ();
+  test_emit_response ();
+
+  /* TODO: test coverage:
+     whitespace and missing whitespace in headers
+     roundtrip from octets to http to octets.  */
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/lsp/mini-http.h b/gcc/lsp/mini-http.h
new file mode 100644
index 00000000000..4a9b6d81e10
--- /dev/null
+++ b/gcc/lsp/mini-http.h
@@ -0,0 +1,235 @@
+/* Implementation of the mini-HTTP-like protocol used by LSP.
+   Copyright (C) 2017-2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_MINI_HTTP_H
+#define GCC_LSP_MINI_HTTP_H
+
+#include "lsp/octets.h"
+#include "lsp/ipc.h"
+
+namespace mini_http {
+
+// FIXME
+
+class message
+{
+public:
+  message (std::vector<uint8_t> content)
+  : m_content (std::move (content))
+  {
+  }
+
+  const std::vector<uint8_t> &
+  get_content () const { return m_content; }
+
+  size_t
+  get_content_length () const { return m_content.size (); }
+
+  octets::buffer
+  make_octets_buffer () const;
+
+  void
+  send_to_ipc_sender (ipc::sender &sender) const;
+
+private:
+  std::vector<uint8_t> m_content;
+};
+
+/* A stateful parser for HTTP that consumes octets and emits a message
+   whenever a full message has been received.  */
+
+class message_parser : public lsp::log_user
+{
+ public:
+  message_parser ();
+
+  std::unique_ptr<message>
+  consume_octet (uint8_t octet);
+
+  // For selftests
+  std::vector<message>
+  parse_buffer (const char *buf);
+
+ private:
+  using buffer_iterator = std::vector<uint8_t>::iterator;
+
+  enum class phase { parsing_headers, parsing_body };
+
+  void
+  consume_header_octet (uint8_t octet);
+
+  std::unique_ptr<message>
+  consume_body_octet (uint8_t octet);
+
+  void
+  parse_header ();
+
+  static bool is_whitespace_p (uint8_t octet);
+
+  static buffer_iterator
+  skip_optional_whitespace (buffer_iterator start,
+			    buffer_iterator end);
+
+  void
+  on_header (buffer_iterator name_start,
+	     buffer_iterator name_end,
+	     buffer_iterator value_start,
+	     buffer_iterator value_end);
+
+  static bool
+  is_specific_header_p (buffer_iterator name_start,
+			buffer_iterator name_end,
+			const char *match_name);
+
+  enum phase m_phase;
+  std::vector<uint8_t> m_pending_data;
+  ssize_t m_content_length;
+};
+
+class sink : public lsp::log_user
+{
+public:
+  virtual ~sink () = default;
+
+  virtual void
+  on_consume_http_message (const message &msg) = 0;
+
+  virtual void
+  on_end_of_http_stream () = 0;
+};
+
+// FIXME: not yet used
+/* A two-way connection.  */
+
+struct connection : public lsp::log_user
+{
+  connection ()
+  : m_outgoing_http_sink (nullptr),
+    m_incoming_http_sink (nullptr)
+  {
+  }
+
+  sink *m_outgoing_http_sink;
+  sink *m_incoming_http_sink;
+};
+
+} // namespace mini_http
+
+
+/* A concrete octets::sink subclass that expects an HTTP-like protocol,
+   with header lines ended by '\r\n', then a '\r\n' line, then
+   the content, dispatching http::messages to an mini_http::sink
+   as they are completed.  */
+
+class octets_to_http : public octets::sink
+{
+public:
+  octets_to_http ()
+  : m_http_sink (nullptr)
+  {
+  }
+
+  void
+  set_http_sink (mini_http::sink *http_sink)
+  {
+    m_http_sink = http_sink;
+  }
+
+  void
+  on_consume_octets (octets::buffer data) final override;
+
+  void
+  on_end_of_octet_stream () final override;
+
+  void
+  set_parser_logger (lsp::logger *logger)
+  {
+    m_parser.set_logger (logger);
+  }
+
+private:
+  mini_http::sink *m_http_sink;
+  mini_http::message_parser m_parser;
+};
+
+/* A mini_http::sink subclass that encodes incoming HTTP messages
+   as octets (adding a Content-Length header) and sends them to
+   an octets::sink.  */
+
+class http_to_octets : public mini_http::sink
+{
+public:
+  http_to_octets ()
+  : m_octet_sink (nullptr)
+  {
+  }
+
+  void
+  set_octet_sink (octets::sink *octet_sink)
+  {
+    m_octet_sink = octet_sink;
+  }
+
+  void
+  on_consume_http_message (const mini_http::message &msg) final override;
+
+  void
+  on_end_of_http_stream () final override;
+
+private:
+  octets::sink *m_octet_sink;
+};
+
+/* FIXME.  */
+
+struct octets_http_adaptor
+{
+  octets_http_adaptor (octets::connection *octets_connection,
+		       mini_http::connection *http_connection)
+  : m_octets_connection (octets_connection),
+    m_http_connection (http_connection)
+  {
+  }
+
+#if 0
+  void
+  set_incoming_octet_sink (octets::sink *incoming_octet_sink)
+  {
+    //m_incoming_octet_sink
+  }
+#endif
+
+  void
+  set_logger (lsp::logger *logger)
+  {
+    m_incoming_octets_to_http.set_logger (logger);
+    m_outgoing_http_to_octets.set_logger (logger);
+  }
+
+  // FIXME:
+  //private:
+  octets::connection *m_octets_connection;
+  mini_http::connection *m_http_connection;
+  octets_to_http m_incoming_octets_to_http;
+  http_to_octets m_outgoing_http_to_octets;
+};
+
+
+#endif  /* GCC_LSP_MINI_HTTP_H  */
diff --git a/gcc/lsp/octets-to-json-rpc.cc b/gcc/lsp/octets-to-json-rpc.cc
new file mode 100644
index 00000000000..0dc55f991ae
--- /dev/null
+++ b/gcc/lsp/octets-to-json-rpc.cc
@@ -0,0 +1,76 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json-parsing.h"
+#include "lsp/octets-to-json-rpc.h"
+
+// FIXME: use this
+
+// class octets_to_json_rpc : public octets::sink
+
+void
+octets_to_json_rpc::on_consume_octets (octets::buffer data)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+  for (auto octet : data)
+    if (auto msg = m_http_parser.consume_octet (octet))
+      consume_http_message (std::move (msg));
+}
+
+void
+octets_to_json_rpc::
+consume_http_message (std::unique_ptr<mini_http::message> msg)
+{
+  auto logger = get_logger ();
+  LSP_LOG_SCOPE (logger);
+
+  // Try to parse MSG as JSON.
+  auto r = json::parse_utf8_string (msg->get_content_length (),
+				    (const char *)msg->get_content ().data (),
+				    false, nullptr);
+  if (!r.m_val)
+    {
+      gcc_assert (r.m_err);
+      m_json_rpc_sink->on_malformed_json (std::move (*r.m_err));
+      return;
+    }
+  gcc_assert (r.m_err == nullptr);
+
+  gcc_assert (m_json_rpc_sink);
+  m_json_rpc_sink->consume_json_message (std::move (*r.m_val));
+}
+
+void
+octets_to_json_rpc::on_end_of_octet_stream ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+
+  gcc_assert (m_json_rpc_sink);
+  m_json_rpc_sink->on_finish ();
+}
diff --git a/gcc/lsp/octets-to-json-rpc.h b/gcc/lsp/octets-to-json-rpc.h
new file mode 100644
index 00000000000..4d46511cb52
--- /dev/null
+++ b/gcc/lsp/octets-to-json-rpc.h
@@ -0,0 +1,53 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_OCTETS_TO_JSON_RPC_H
+#define GCC_LSP_OCTETS_TO_JSON_RPC_H
+
+#include "lsp/octets.h"
+#include "lsp/mini-http.h"
+#include "lsp/json-rpc.h"
+
+// FIXME:
+
+class octets_to_json_rpc : public octets::sink
+{
+public:
+  octets_to_json_rpc ()
+  : m_json_rpc_sink (nullptr)
+  {}
+
+  void
+  set_json_rpc_sink (json_rpc::sink *sink)
+  {
+    m_json_rpc_sink = sink;
+  }
+
+  void on_consume_octets (octets::buffer data) final override;
+  void on_end_of_octet_stream () final override;
+
+private:
+  void consume_http_message (std::unique_ptr<mini_http::message> http_msg);
+
+  mini_http::message_parser m_http_parser;
+  json_rpc::sink *m_json_rpc_sink;
+};
+
+#endif  /* GCC_LSP_OCTETS_TO_JSON_RPC_H  */
diff --git a/gcc/lsp/octets.cc b/gcc/lsp/octets.cc
new file mode 100644
index 00000000000..ea656c075f8
--- /dev/null
+++ b/gcc/lsp/octets.cc
@@ -0,0 +1,72 @@
+/* Low-level classes for working with octets.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "lsp/octets.h"
+
+#if CHECKING_P
+
+void
+octets::selftest::assert_buffer_eq_at (const ::selftest::location &loc,
+				       const octets::buffer &buf,
+				       const char *expected_content)
+{
+  ASSERT_EQ_AT (loc, buf.size (), strlen (expected_content));
+  ASSERT_TRUE_AT (loc, !strncmp (expected_content,
+				 (const char *)buf.data (),
+				 buf.size ()));
+}
+
+// class octets::selftest::test_sink : public octets::sink
+
+void
+octets::selftest::test_sink::on_consume_octets (buffer data)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  // Append data to m_buffer
+  m_buffer.insert<buffer::iterator> (m_buffer.end (),
+				     data.begin (), data.end ());
+}
+
+void
+octets::selftest::test_sink::on_end_of_octet_stream ()
+{
+  LSP_LOG_SCOPE (get_logger ());
+  gcc_assert (!m_seen_eof);
+  m_seen_eof = true;
+}
+
+void
+octets::selftest::test_sink::
+assert_buffer_eq_at (const ::selftest::location &loc,
+		     const char *expected_content)
+{
+  LSP_LOG_SCOPE (get_logger ());
+  octets::selftest::assert_buffer_eq_at (loc, m_buffer, expected_content);
+}
+
+// FIXME: selftests
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/lsp/octets.h b/gcc/lsp/octets.h
new file mode 100644
index 00000000000..d4eb307d207
--- /dev/null
+++ b/gcc/lsp/octets.h
@@ -0,0 +1,117 @@
+/* Low-level IPC classes (octets).
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_OCTETS_H
+#define GCC_LSP_OCTETS_H
+
+#include "lsp/lsp-logging.h"
+#include "selftest.h"
+
+namespace octets {
+
+using buffer = std::vector<uint8_t>;
+
+/* Abstract base class for an object that can consume octets.  */
+
+class sink : public lsp::log_user
+{
+public:
+  virtual void on_consume_octets (buffer data) = 0;
+  virtual void on_end_of_octet_stream () = 0;
+};
+
+// FIXME: not yet used
+/* A two-way connection.  */
+
+struct connection : public lsp::log_user
+{
+  connection ()
+  : m_outgoing_octet_sink (nullptr),
+    m_incoming_octet_sink (nullptr)
+  {
+  }
+
+  sink *m_outgoing_octet_sink;
+  sink *m_incoming_octet_sink;
+};
+
+#if 0
+// FIXME: an octets::connection implemented in terms of an underlying ipc::connection
+
+class ipc_connection : public connection
+{
+public:
+  ipc_connection (file_descriptor outgoing_fd,
+		  file_descriptor incoming_fd)
+  : m_ipc_connection (outgoing_fd, incoming_fd)
+    m_outgoing_octets_to_ipc (m_ipc_connection.m_outgoing_sender)
+  {
+  }
+
+private:
+  ipc::connection m_ipc_connection;
+  ipc::octets_to_sender m_outgoing_octets_to_ipc;
+};
+#endif
+
+#if CHECKING_P
+
+namespace selftest { // octets::selftest
+
+// FIXME
+
+void
+assert_buffer_eq_at (const ::selftest::location &loc,
+		     const buffer &buf,
+		     const char *expected_content);
+
+/* An octets::sink for selftests, which accumulates data into
+   an internal buffer, and tracks whether EOF has occurred.  */
+
+class test_sink : public sink
+{
+public:
+  test_sink () : m_seen_eof (false) {}
+
+  void
+  on_consume_octets (buffer data) override;
+
+  void
+  on_end_of_octet_stream () override;
+
+  void
+  assert_buffer_eq_at (const ::selftest::location &loc,
+		       const char *expected_content);
+
+  bool
+  seen_eof_p () const { return m_seen_eof; }
+
+private:
+  octets::buffer m_buffer;
+  bool m_seen_eof;
+};
+
+} // namespace octets::selftest
+
+#endif /* #if CHECKING_P */
+
+} // namespace octets
+
+#endif  /* GCC_LSP_OCTETS_H  */
diff --git a/gcc/lsp/subprocess.cc b/gcc/lsp/subprocess.cc
new file mode 100644
index 00000000000..f10df99a416
--- /dev/null
+++ b/gcc/lsp/subprocess.cc
@@ -0,0 +1,207 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_LIST
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "subprocess.h"
+#include "diagnostic-core.h"
+#include "selftest.h"
+
+// class subprocess
+
+subprocess
+subprocess::launch (std::list<std::string> args,
+		    lsp::logger *logger)
+{
+  LSP_LOG_SCOPE (logger);
+  if (logger)
+    {
+      // TODO
+    }
+
+  // TODO
+  pex_obj *pex = pex_init (PEX_USE_PIPES,
+			   progname, nullptr); // FIXME: temp_filename);
+  if (!pex)
+    fatal_error (input_location, "%<pex_init%> failed: %m");
+
+  /* Build a NULL-terminated array of const char * for pex.  */
+  std::vector<char *> argvec;
+  for (auto &arg : args)
+    argvec.push_back (const_cast <char *> (arg.c_str ()));
+  argvec.push_back (nullptr);  
+
+  FILE *pex_stdin = pex_input_pipe (pex, 1);
+  gcc_assert (pex_stdin); // FIXME
+
+  int err;
+  const char *errmsg
+    = pex_run (pex,
+	       PEX_SEARCH | PEX_BINARY_INPUT | PEX_BINARY_OUTPUT,
+	       argvec[0],
+	       argvec.data (),
+	       nullptr, nullptr, &err);
+
+  // FIXME: error-handling
+  if (errmsg)
+    fatal_error (input_location, "%<pex_run%> failed: %m: \"%s\", err: %i", errmsg, err);
+  if (err)
+    fatal_error (input_location, "%<pex_run%> failed: %m: errmsg: errmsg: null, err: %i", err);
+
+  return subprocess (std::move (args), pex, pex_stdin, logger);
+}
+
+subprocess::subprocess (std::list<std::string> args,
+			pex_obj *pex,
+			FILE *pex_stdin,
+			lsp::logger *logger)
+: log_user (logger),
+  m_args (std::move (args)),
+  m_pex (pex),
+  m_sender_to_stdin (file_descriptor (fileno (pex_stdin))),
+  m_receiver_from_stdout (file_descriptor (fileno (pex_read_output (m_pex, 1))))
+{
+  m_sender_to_stdin.set_logger (logger);
+  m_receiver_from_stdout.set_logger (logger);
+}
+
+subprocess::subprocess (subprocess &&other)
+: log_user (other.get_logger ()),
+  m_args (std::move (other.m_args)),
+  m_pex (other.m_pex),
+  m_sender_to_stdin (std::move (other.m_sender_to_stdin)),
+  m_receiver_from_stdout (std::move (other.m_receiver_from_stdout))
+{
+  other.m_pex = nullptr;
+}
+
+subprocess::~subprocess ()
+{
+  if (m_pex)
+    pex_free (m_pex);
+  //fclose (m_stdin_file);
+}
+
+void
+subprocess::set_debug_name (std::string name)
+{
+  m_sender_to_stdin.set_debug_name ("stdin to " + name);
+  m_receiver_from_stdout.set_debug_name ("stdout of " + name);
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Selftests.  */
+
+static void
+test_non_existent ()
+{
+  lsp::logger *logger = new lsp::logger (stderr, "selftest", nullptr, nullptr);
+
+  std::list<std::string> args;
+  args.push_back ("this command should not exist");
+  auto p = subprocess::launch (std::move (args), logger);
+
+  octets::selftest::test_sink stdout_dst;
+  stdout_dst.set_logger (logger);
+  p.get_sender_to_stdin ()->add_test_data_to_queue ("hello world\n");
+  p.get_receiver_from_stdout ()->set_octet_sink (&stdout_dst);
+  // TODO: split out into two lines, etc
+
+  ipc::event_poll ep;
+  ep.set_logger (logger);
+  ep.add_endpoint (p.get_sender_to_stdin ());
+  ep.add_endpoint (p.get_receiver_from_stdout ());
+
+  // FIXME:
+  ep.handle_pending_events (false); // to send data
+#if 0
+  pex_free (p.m_pex);
+  p.m_pex = nullptr;
+#endif
+  ep.handle_pending_events (false); // to receive data
+  //s.handle_pending_events (false); // to receive data
+  // FIXME: seems to be failing with SIGPIPE
+
+  stdout_dst.assert_buffer_eq_at (SELFTEST_LOCATION, "hello world!\n");
+
+  delete logger;
+}
+
+static void
+test_simple (lsp::logger *logger)
+{
+  std::list<std::string> args;
+  args.push_back ("cat");
+  auto p = subprocess::launch (std::move (args), logger);
+
+  octets::selftest::test_sink stdout_dst;
+  stdout_dst.set_logger (logger);
+  p.get_sender_to_stdin ()->add_test_data_to_queue ("hello world\n");
+  p.get_sender_to_stdin ()->add_close_to_queue ();
+  p.get_receiver_from_stdout ()->set_octet_sink (&stdout_dst);
+  // TODO: split out into two lines, etc
+
+  ipc::event_poll evp;
+  evp.set_logger (logger);
+  evp.add_endpoint (p.get_sender_to_stdin ());
+  evp.add_endpoint (p.get_receiver_from_stdout ());
+
+  // FIXME:
+  for (int i = 0; i < 100; ++i)
+    {
+      evp.handle_pending_events (false);
+      if (stdout_dst.seen_eof_p ())
+	break;
+    }
+  ASSERT_TRUE (stdout_dst.seen_eof_p ());
+
+  stdout_dst.assert_buffer_eq_at (SELFTEST_LOCATION, "hello world\n");
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+lsp_subprocess_cc_tests ()
+{
+  lsp::logger *logger = new lsp::logger (stderr, "selftest", nullptr, nullptr);
+
+#if 0
+  test_non_existent ();
+#endif
+  test_simple (logger);
+
+  /* TODO: test coverage:
+     - program not found
+     - read from /dev/null
+     - write to /dev/null
+  */
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
+
diff --git a/gcc/lsp/subprocess.h b/gcc/lsp/subprocess.h
new file mode 100644
index 00000000000..40f5898ddc9
--- /dev/null
+++ b/gcc/lsp/subprocess.h
@@ -0,0 +1,75 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_LSP_SUBPROCESS_H
+#define GCC_LSP_SUBPROCESS_H
+
+#include "lsp/octets.h"
+#include "lsp/ipc.h"
+
+// FIXME
+
+class subprocess : public lsp::log_user
+{
+public:
+  static subprocess
+  launch (std::list<std::string> args,
+	  lsp::logger *logger);
+  // FIXME: make it a result<>, for dealing with errors
+
+  subprocess () = delete;
+  subprocess (const subprocess &) = delete;
+  subprocess (subprocess &&);
+  ~subprocess ();
+
+  subprocess& operator= (const subprocess &) = delete;
+  subprocess& operator= (subprocess &&) = delete;
+
+  ipc::sender *
+  get_sender_to_stdin () { return &m_sender_to_stdin; }
+
+  ipc::receiver *
+  get_receiver_from_stdout () { return &m_receiver_from_stdout; }
+
+  void
+  add_to_event_poll (ipc::event_poll &evp)
+  {
+    evp.add_endpoint (&m_sender_to_stdin);
+    evp.add_endpoint (&m_receiver_from_stdout);
+  }
+
+  void
+  set_debug_name (std::string name);
+
+// FIXME:
+//private:
+  subprocess (std::list<std::string> args,
+	      pex_obj *pex,
+	      FILE *pex_stdin,
+	      lsp::logger *logger);
+
+  std::list<std::string> m_args;
+  pex_obj *m_pex;
+  ipc::sender m_sender_to_stdin;
+  ipc::receiver m_receiver_from_stdout;
+  std::string m_debug_name;
+};
+
+#endif  /* GCC_LSP_SUBPROCESS_H  */
diff --git a/gcc/opts-diagnostic.cc b/gcc/opts-diagnostic.cc
index 421f11623f5..ccf64de7863 100644
--- a/gcc/opts-diagnostic.cc
+++ b/gcc/opts-diagnostic.cc
@@ -50,7 +50,7 @@ class gcc_extra_keys : public diagnostics::output_spec::key_handler
 public:
   gcc_extra_keys ()
   : m_analyzer_graphs (false),
-  : m_ast (false),
+    m_ast (false),
     m_cfgs (false),
     m_include_graph (false)
   {
diff --git a/gcc/selftest-run-tests.cc b/gcc/selftest-run-tests.cc
index e39a94f8688..1503600a3dd 100644
--- a/gcc/selftest-run-tests.cc
+++ b/gcc/selftest-run-tests.cc
@@ -86,7 +86,16 @@ selftest::run_tests ()
   xml_cc_tests ();
   graphviz_cc_tests ();
   pub_sub_cc_tests ();
-
+  uris_cc_tests ();
+
+  lsp_http_server_cc_tests ();
+  lsp_json_rpc_cc_tests ();
+#if 0
+  lsp_lsp_cc_tests ();
+#endif
+  lsp_spec_cc_tests ();
+  lsp_subprocess_cc_tests ();
+ 
   /* Mid-level data structures.  */
   input_cc_tests ();
   vec_perm_indices_cc_tests ();
diff --git a/gcc/selftest.h b/gcc/selftest.h
index 8891d0b7b6f..3bb1d7c5148 100644
--- a/gcc/selftest.h
+++ b/gcc/selftest.h
@@ -240,6 +240,12 @@ extern void ipa_modref_tree_cc_tests ();
 extern void json_cc_tests ();
 extern void json_parser_cc_tests ();
 extern void json_pointer_parsing_cc_tests ();
+extern void lsp_http_server_cc_tests ();
+extern void lsp_json_rpc_cc_tests ();
+extern void lsp_lsp_cc_tests ();
+extern void lsp_spec_cc_tests ();
+extern void lsp_subprocess_cc_tests ();
+extern void lsp_uri_cc_tests ();
 extern void opt_suggestions_cc_tests ();
 extern void optinfo_emit_json_cc_tests ();
 extern void opts_cc_tests ();
@@ -267,6 +273,7 @@ extern void tree_cc_tests ();
 extern void tree_cfg_cc_tests ();
 extern void tristate_cc_tests ();
 extern void typed_splay_tree_cc_tests ();
+extern void uris_cc_tests ();
 extern void vec_cc_tests ();
 extern void vec_perm_indices_cc_tests ();
 extern void wide_int_cc_tests ();
diff --git a/gcc/testsuite/gcc.dg/lsp/cli-client.py b/gcc/testsuite/gcc.dg/lsp/cli-client.py
new file mode 100644
index 00000000000..447aa9a708a
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/cli-client.py
@@ -0,0 +1,270 @@
+import json
+import os
+import re
+import select
+import subprocess
+import sys
+import time
+
+import lsp
+from lsp import log
+
+class ByteConnection:
+    def __init__(self, fd_receive, fd_send):
+        self.fd_receive = fd_receive
+        self.fd_send = fd_send
+        self.outgoing_data = bytes()
+        self.incoming_byte_handler = None
+
+    def __repr__(self):
+        return f'ByteConnection({self.fd_receive}, {self.fd_send}, {self.outgoing_data}, {self.incoming_data})'
+
+    @staticmethod
+    def from_subprocess(proc):
+        return ByteConnection(fd_receive = proc.stdout.fileno(),
+                              fd_send = proc.stdin.fileno())
+
+    def push_data(self, data: bytes):
+        log('ByteConnection.push_data', repr(data))
+        self.outgoing_data += data
+
+    def pop_data(self, data: bytes):
+        result = self.outgoing_buffers[0]
+        self.outgoing_buffers = self.outgoing_buffers[1:]
+        return result
+
+    def blocking_read(self):
+        received = os.read(self.fd_receive, 4096)
+        log('ByteConnection.on_receive', f'{len(received)} bytes read from fd {self.fd_receive}')
+        log('ByteConnection.on_receive', repr(received))
+        if self.incoming_byte_handler:
+            self.incoming_byte_handler(received)
+
+    def blocking_write(self):
+        if self.outgoing_data:
+            bytes_written = os.write(self.fd_send, self.outgoing_data)
+            self.outgoing_data = self.outgoing_data[bytes_written:]
+            log('ByteConnection.send', f'{bytes_written} bytes written to fd {self.fd_send}')
+
+class HttpMessage:
+    def __init__(self, content: bytes):
+        self.content = content
+
+    def __repr__(self):
+        return f'HttpMessage({self.content})'
+
+    def to_bytes(self) -> bytes:
+        return f'Content-Length: {len(self.content)}\r\n\r\n' + self.content
+
+class HttpParser:
+    def __init__(self):
+        self.phase = 'headers'
+        self.pending_data = bytes()
+        self.content_length = None
+
+    def consume_octet(self, octet, on_http_message):
+        #log('HttpParser.consume_octet', repr(octet))
+        self.pending_data += octet.to_bytes(length=1, byteorder='big')
+        #print(f'{self.pending_data=}')
+        if self.phase == 'headers':
+            if self.pending_data.endswith(b'\r\n'):
+                if self.pending_data == b'\r\n':
+                    # blank line; end of headers                    
+                    #log('HttpParser.consume_octet', 'blank line: end of headers')
+                    self.pending_data = bytes()
+                    self.phase = 'body'
+                else:
+                    # Header (or initial verb line)
+                    self.parse_header(self.pending_data[:-2])
+                    self.pending_data = bytes()
+        else:
+            assert self.phase == 'body'
+            assert self.content_length is not None
+            if len(self.pending_data) == self.content_length:
+                msg = HttpMessage(content=self.pending_data)
+                self.phase = 'headers'
+                self.pending_data = bytes()
+                self.content_length = None
+                if on_http_message:
+                    on_http_message(msg)
+
+    def parse_header(self, line: bytes):
+        # header-field   = field-name ":" OWS field-value OWS
+        log('HttpParser.parse_header', repr(line))
+        m = re.match(b'Content-Length:\s+([0-9]+)', line)
+        if m:
+            self.content_length = int(m.group(1))
+        # FIXME: discard other headers for now
+
+class HttpConnection:
+    def __init__(self, byte_conn: ByteConnection):
+        self.byte_conn = byte_conn
+        self.outgoing_messages = []
+        self.parser = HttpParser()
+        self.incoming_http_message_handler = None
+
+    def push_message (self, msg: HttpMessage):
+        #log('HttpConnection.push_message', repr(msg))
+        self.byte_conn.push_data(msg.to_bytes().encode('utf-8'))
+
+    def on_incoming_bytes(self, data: bytes):
+        #log('HttpConnection.on_incoming_bytes', repr(data))
+        for b in data:
+            self.parser.consume_octet(b, self.incoming_http_message_handler)
+
+class Selector:
+    def __init__(self):
+        self.byte_connections = set()
+        self.poll = select.poll()
+        self.fd_to_connection = {}
+
+    def add_byte_connection(self, conn: ByteConnection):
+        self.byte_connections.add(conn)
+        self.poll.register(conn.fd_receive)
+        self.poll.register(conn.fd_send)
+        self.fd_to_connection[conn.fd_receive] = conn
+        self.fd_to_connection[conn.fd_send] = conn
+
+    def serve_forever(self):
+        log('Selector.serve_forever', '')
+        while 1:
+            res = self.poll.poll()
+            for fd, event in res:
+                # print(f'{fd=} {event=}')
+                if event & select.POLLIN:
+                    print('POLLIN')
+                    conn = self.fd_to_connection[fd]
+                    #print(f'{conn=}')
+                    conn.blocking_read()
+                if event & select.POLLOUT:
+                    #print('POLLOUT')
+                    conn = self.fd_to_connection[fd]
+                    #print(f'{conn=}')
+                    conn.blocking_write()
+
+class LspMessage:
+    def __init__(self, method: str, params, id=None):
+        self.method = method
+        self.params = params
+        self.id = id
+
+    def __repr__(self):
+        return f'LspMessage({self.method:s}, {self.params}, {self.id})'
+
+    def to_json(self):
+        res = {"jsonrpc": "2.0",
+               "method": self.method,
+               "params": self.params}
+        if self.id is not None:
+            res['id'] = self.id
+        return res
+
+    '''
+    @staticmethod
+    def from_json_request(js_obj):
+        return LspMessage(method=js_obj['method'],
+                          params=js_obj['params'],
+                          id=js_obj.get('id'))
+    '''
+
+class LspClient:
+    def __init__(self, http: HttpConnection):
+        self.http = http
+        self.next_id = 0
+        # map from id to on_response callbacks
+        self.pending_requests = {}
+
+        # Start initialization handshake
+        self.send_request('initialize',
+                          lsp.InitializeParams(processId=os.getpid()),
+                          on_response=self.on_initialized)
+
+    def send_request(self, method_name: str, params, on_response):
+        log('LspClient.send_request', method_name)
+        lsp_msg = LspMessage(method_name, params.to_json(), self.next_id)
+        self.next_id += 1
+        self.pending_requests[lsp_msg.id] = on_response
+        self._queue_message(lsp_msg)
+        
+    def send_notification(self, method_name: str, params):
+        log('LspClient.send_notification', method_name)
+        lsp_msg = LspMessage(method_name, params.to_json())
+        self._queue_message(lsp_msg)
+
+    def _queue_message(self, msg: LspMessage):
+        log('LspClient.queue_message', msg)
+        js_msg = json.dumps(msg.to_json())
+        self.http.push_message(HttpMessage(content=js_msg))
+
+    def on_incoming_http_message(self, http_msg: HttpMessage):
+        #log('LspClient.on_incoming_http_message', repr(msg))
+        js_msg = json.loads(http_msg.content.decode('utf-8'))
+        try:
+            print(f'{js_msg=}')
+            id_ = js_msg.get('id')
+            if id_ is None:
+                # notification
+                self.on_notification(js_msg)
+            else:
+                # response
+                if id_ in self.pending_requests:
+                    pending_req = self.pending_requests.pop(id_)
+                    if pending_req:
+                        # Could be a success or an error:
+                        pending_req(js_msg)
+                else:
+                    raise Error(f'unexpected response id: {id_=}' )
+        except:
+            raise foo
+
+    def on_initialized(self, js_msg):
+        log('LspClient.on_initialized', repr(js_msg))
+
+        with open(FILENAME) as f:
+            text = f.read()
+
+        self.send_notification('textDocument/didOpen',
+                               lsp.DidOpenTextDocumentParams(lsp.TextDocumentItem(uri=URI,
+                                                                                  languageId='c', # FIXME
+                                                                                  version=0,
+                                                                                  text=text)))
+        self.send_request('textDocument/definition',
+                          lsp.TextDocumentPositionParams(textDocument=lsp.TextDocumentIdentifier(URI),
+                                                         position=lsp.Position(line=8, character=19)),
+                          on_response=self.on_definition)
+
+    def on_definition(self, js_msg):
+        log('LspClient.on_definition', repr(js_msg))
+
+    def on_notification(self, js_msg):
+        log('LspClient.on_notification', repr(js_msg))
+
+FILENAME = 'gcc/testsuite/gcc.dg/lsp/test.c'                                               
+URI = 'file://' + os.path.abspath(FILENAME)
+
+if 0:
+    args=['clangd', '--log=verbose']
+else:
+    args = ['/home/david/coding-3/gcc-newgit-lsp/build/gcc/cc1',
+            '-quiet',
+            '-flsp=0',
+            '/home/david/coding-3/gcc-newgit-lsp/src/gcc/testsuite/gcc.dg/lsp/test.c']
+
+proc = subprocess.Popen(args,
+                        bufsize=0, # unbuffered
+                        stdin=subprocess.PIPE,
+                        stdout=subprocess.PIPE)
+
+byte_conn = ByteConnection.from_subprocess(proc)
+
+http_conn = HttpConnection(byte_conn)
+
+byte_conn.incoming_byte_handler = http_conn.on_incoming_bytes
+
+client = LspClient(http_conn)
+
+http_conn.incoming_http_message_handler = client.on_incoming_http_message
+
+s = Selector()
+s.add_byte_connection(byte_conn)
+s.serve_forever()
diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.py b/gcc/testsuite/gcc.dg/lsp/lsp.py
new file mode 100644
index 00000000000..088a95b335f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/lsp.py
@@ -0,0 +1,239 @@
+# A python module for implementing LSP clients
+
+import json
+import os
+import subprocess
+import sys
+
+import jsonrpc # pip install json-rpc
+import requests
+
+def log(what, msg):
+    print('%s: %s' % (what, msg), file=sys.stderr)
+
+# Various types to model the LSP interface
+
+class InitializeParams:
+    def __init__(self, processId: int):
+        self.processId = processId
+
+    def to_json(self):
+        return {'processId': self.processId}#,
+    #'clientInfo': self.clientInfo.to_json () }
+
+class Position:
+    def __init__(self, line, character):
+        self.line = line
+        self.character = character
+
+    def __repr__(self):
+        return 'Position(%r, %r)' % (self.line, self.character)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {'line': self.line, 'character': self.character}
+
+    @staticmethod
+    def from_json(js):
+        return Position(js['line'], js['character'])
+
+class Range:
+    def __init__(self, start, end):
+        self.start = start
+        self.end = end
+
+    def __repr__(self):
+        return 'Range(%r, %r)' % (self.start, self.end)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    @staticmethod
+    def from_json(js):
+        return Range(Position.from_json(js['start']),
+                     Position.from_json(js['end']))
+
+class Location:
+    def __init__(self, uri, range):
+        self.uri = uri
+        self.range = range
+
+    def __repr__(self):
+        return 'Location(%r, %r)' % (self.uri, self.range)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    @staticmethod
+    def from_json(js):
+        return Location(js['uri'], Range.from_json(js['range']))
+
+    def dump(self, msg):
+        print('%s:%i:%i: %s' % (self.uri, self.range.start.line,
+                                self.range.start.character, msg))
+        # FIXME: underline
+        # linecache uses 1-based line numbers, whereas LSP uses
+        # 0-based line numbers
+        import linecache
+        line = linecache.getline(self.uri, self.range.start.line + 1)
+        print('line: %r' % line)
+
+class TextDocumentIdentifier:
+    def __init__(self, uri):
+        self.uri = uri
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {'uri': self.uri}
+
+DocumentUri = str
+
+class TextDocumentItem:
+    def __init__(self,
+                 uri: DocumentUri,
+	         languageId: str,
+	         version: int,
+	         text: str):
+        self.uri = uri
+        self.languageId = languageId
+        self.version = version
+        self.text = text
+
+    def to_json(self):
+        return {"uri" : self.uri,
+                "languageId" : self.languageId,
+                "version": self.version,
+                "text": self.text}
+
+class DidOpenTextDocumentParams:
+    def __init__(self, textDocument: TextDocumentItem):
+        self.textDocument = textDocument
+
+    def to_json(self):
+        return {"textDocument" : self.textDocument.to_json()}
+
+class TextDocumentPositionParams:
+    def __init__(self,
+                 textDocument: TextDocumentIdentifier,
+                 position: Position):
+        self.textDocument = textDocument
+        self.position = position
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {"textDocument" : self.textDocument.to_json(),
+                "position" : self.position.to_json()}
+
+
+class Transport:
+    def post_request(self, data: str):
+        raise NotImplementedError
+    def post_notification(self, data: str):
+        raise NotImplementedError
+
+class UrlTransport(Transport):
+    def __init__(self, url):
+        self.url = url
+
+    def post_request(self, data: str):
+        headers = {'content-type': 'application/json'}
+        response = requests.post(self.url, data=data, headers=headers)
+        return response
+    
+class SubprocessTransport(Transport):
+    def __init__(self, args):
+        self.args = args
+        self.proc = subprocess.Popen(args,
+                                     bufsize=0, # unbuffered
+                                     stdin=subprocess.PIPE,
+                                     stdout=subprocess.PIPE)
+
+    def post_request(self, data: str):
+        encoded = data.encode('utf-8')
+        byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded
+        log('toy-ide', 'sending to subprocess: %r' % byte_msg)
+        self.proc.stdin.write(byte_msg)
+        self.proc.stdin.flush()
+        log('toy-ide', 'sent to subprocess')
+        #self.proc.stdout.flush()
+        resp = os.read(self.proc.stdout.fileno(), 4096) # FIXME
+        log('toy-ide', f'received from subprocess: {resp=}')
+        #headers = {'content-type': 'application/json'}
+        #response = requests.post(self.url, data=data, headers=headers)
+        #return response
+
+    def post_notification(self, data: str):
+        encoded = data.encode('utf-8')
+        byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded
+        log('toy-ide', 'sending to subprocess: %r' % byte_msg)
+        self.proc.stdin.write(byte_msg)
+        self.proc.stdin.flush()
+        log('toy-ide', 'sent to subprocess')
+        #resp = self.proc.stdout.read();
+        #log('toy-ide', f'received from subprocess: {resp=}')
+        '''
+        headers = {'content-type': 'application/json'}
+        response = requests.post(self.url, data=data, headers=headers)
+        return response
+        '''
+    
+# A wrapper for making LSP calls against a server
+
+class Proxy:
+    def __init__(self, transport: Transport):
+        self.transport = transport
+        self.next_id = 0
+
+        # TODO: initialization
+        params = InitializeParams()
+        json_resp = self.post_request('initialize', params.to_json())
+        print(json_resp)
+
+    def make_request(self, method, params):
+        json_req = {"method": method,
+                    "params": params,
+                    "jsonrpc": "2.0",
+                    "id": self.next_id}
+        self.next_id += 1
+        return json_req
+
+    def make_notification(self, method, params):
+        json_req = {"method": method,
+                    "params": params,
+                    "jsonrpc": "2.0"}
+        return json_req
+
+    def post_request(self, method, params):
+        json_obj = self.make_request(method, params)
+        response = self.transport.post_request(json.dumps(json_obj))
+        #print('response: %r' % response)
+        #print('response.json(): %r' % response.json())
+        #return response.json()
+
+    def post_notification(self, method, params):
+        json_obj = self.make_notification(method, params)
+        response = self.transport.post_notification(json.dumps(json_obj))
+        #print('response: %r' % response)
+        #print('response.json(): %r' % response.json())
+        #return response.json()
+
+    def textDocument_didOpen(self, params: DidOpenTextDocumentParams):
+        self.post_notification('textDocument/didOpen',
+                               params.to_json())
+
+    def textDocument_definition(self, params: TextDocumentPositionParams):
+        json_resp = self.post_request('textDocument/definition',
+                                      params.to_json())
+        print(f'{json_resp=}')
+        raise foo
+        # Expect either a Location or a list of Location
+        if isinstance(json_resp, list):
+            return [Location.from_json(item) for item in json_resp]
+        else:
+            return Location.from_json(json_resp)
diff --git a/gcc/testsuite/gcc.dg/lsp/test.c b/gcc/testsuite/gcc.dg/lsp/test.c
new file mode 100644
index 00000000000..bb80bd06086
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.c
@@ -0,0 +1,12 @@
+/* */
+
+struct foo
+{
+  int color;
+  int shape;
+};
+
+int test (struct foo *ptr)
+{
+  ptr->
+}
diff --git a/gcc/testsuite/gcc.dg/lsp/test.py b/gcc/testsuite/gcc.dg/lsp/test.py
new file mode 100644
index 00000000000..c7f00670079
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.py
@@ -0,0 +1,28 @@
+from lsp import Proxy, TextDocumentIdentifier, Position, Range, Location
+
+def main():
+    # This assumes that we're running this:
+    # (hardcoding the particular source file for now):
+    # ./xgcc -B. -c ../../src/gcc/testsuite/gcc.dg/lsp/test.c \
+    #    -flsp=4000 -fblt -wrapper gdb,--args
+
+    url = "http://localhost:4000/jsonrpc"
+    proxy = Proxy(url)
+
+    # FIXME: filename/uri (currently a particular relative location)
+    FILENAME = '../../src/gcc/testsuite/gcc.dg/lsp/test.c'
+
+    # Ask for the location of a usage of "struct foo" (0-based lines)
+    result = proxy.goto_definition(TextDocumentIdentifier(FILENAME),
+                                   Position(8, 16))
+
+    # We expect to get back the location of where "struct foo" is defined
+    print(result)
+    # FIXME: filename/uri (currently a particular relative location)
+    assert result == [Location(FILENAME,
+                               Range(Position(2, 0), Position(6, 1)))]
+    for loc in result:
+        loc.dump("definition")
+
+if __name__ == "__main__":
+    main()
diff --git a/gcc/testsuite/gcc.dg/lsp/toy-ide.py b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
new file mode 100644
index 00000000000..d49cf48fceb
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
@@ -0,0 +1,182 @@
+# A toy IDE implemented in PyGTK
+
+import os
+import sys
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GtkSource", "4")
+from gi.repository import GLib, Gio, Gtk, GtkSource
+
+import lsp
+from lsp import log
+
+# This would typically be its own file
+MENU_XML = """
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="app-menu">
+    <section>
+      <item>
+        <attribute name="action">win.goto_definition</attribute>
+        <attribute name="label" translatable="yes">Goto Definition</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="action">app.about</attribute>
+        <attribute name="label" translatable="yes">_About</attribute>
+      </item>
+      <item>
+        <attribute name="action">app.quit</attribute>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="accel">&lt;Primary&gt;q</attribute>
+    </item>
+    </section>
+  </menu>
+</interface>
+"""
+
+class ToyIdeWindow(Gtk.ApplicationWindow):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # This will be in the windows group and have the "win" prefix
+        goto_definition_action = Gio.SimpleAction.new_stateful(
+            "goto_definition", None, GLib.Variant.new_boolean(False)
+        )
+        goto_definition_action.connect("change-state", self.on_goto_definition)
+        self.add_action(goto_definition_action)
+
+        self.buf = GtkSource.Buffer()
+
+        self.filename = 'gcc/testsuite/gcc.dg/lsp/test.c'
+        with open(self.filename) as f:
+            text = f.read()
+            self.buf.set_text(text)
+
+        # FIXME:
+        self.sv = GtkSource.View(buffer=self.buf)
+        self.sv.set_show_line_numbers(True)
+        self.add(self.sv)
+        self.sv.show()
+
+        if 1:
+            transport = lsp.SubprocessTransport(['clangd', '--log=verbose'])
+            #transport = lsp.SubprocessTransport(['/home/david/coding-3/gcc-newgit-lsp/build/gcc/cc1', '-quiet, '-flsp=0', '/home/david/coding-3/gcc-newgit-lsp/src/gcc/testsuite/gcc.dg/lsp/test.c'])
+            #transport = lsp.SubprocessTransport(['cat'])
+        else:
+            transport = lsp.UrlTransport('http://localhost:4000/jsonrpc')
+        self.lsp = lsp.Proxy(transport)
+
+        textDocument = lsp.TextDocumentItem(uri=self.get_uri(),
+                                            languageId='c', # FIXME
+                                            version=0,
+                                            text=text)
+        params = lsp.DidOpenTextDocumentParams(textDocument)
+        self.lsp.textDocument_didOpen(params)
+
+    def get_uri(self):
+        return 'file://' + os.path.abspath(self.filename)        
+
+    def on_change_label_state(self, action, value):
+       action.set_state(value)
+       self.label.set_text(value.get_string())
+
+    def get_cursor_position(self):
+        """Get the position of the cursor within the buffer
+        as an lsp.Position"""
+        mark = self.buf.get_insert()
+        #print(mark)
+        iter = self.buf.get_iter_at_mark(mark)
+        #print(iter)
+
+        #print('line: %r' % iter.get_line()) # 0-based line
+        #print('line_offset: %r' % iter.get_line_offset()) # 0-based offse
+
+        return lsp.Position(iter.get_line(), iter.get_line_offset())
+
+    def on_goto_definition(self, action, value):
+        log("ToyIdeWindow", "on_goto_definition")
+
+        # FIXME: need to sort out paths between the LSP server and client
+        uri = self.get_uri()
+        params = lsp.TextDocumentPositionParams(textDocument=lsp.TextDocumentIdentifier(uri),
+                                                position=self.get_cursor_position())
+        locs = self.lsp.textDocument_definition(params)
+        print(locs)
+        if len(locs) == 1:
+            loc = locs[0]
+            # Both lsp.Range and Gtk.TextBuffer.select_range are inclusive
+            # on the start, exclusive on the end-point
+            self.buf.select_range(
+                self.buf.get_iter_at_line_offset(loc.range.start.line,
+                                                 loc.range.start.character),
+                self.buf.get_iter_at_line_offset(loc.range.end.line,
+                                                 loc.range.end.character))
+
+class ToyIdeApp(Gtk.Application):
+    def __init__(self, *args, **kwargs):
+        super().__init__(
+            *args,
+            application_id="org.example.myapp",
+            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
+            **kwargs
+        )
+        self.window = None
+
+        self.add_main_option(
+            "test",
+            ord("t"),
+            GLib.OptionFlags.NONE,
+            GLib.OptionArg.NONE,
+            "Command line test",
+            None,
+        )
+
+    def do_startup(self):
+        Gtk.Application.do_startup(self)
+
+        action = Gio.SimpleAction.new("about", None)
+        action.connect("activate", self.on_about)
+        self.add_action(action)
+
+        action = Gio.SimpleAction.new("quit", None)
+        action.connect("activate", self.on_quit)
+        self.add_action(action)
+
+        builder = Gtk.Builder.new_from_string(MENU_XML, -1)
+        self.set_app_menu(builder.get_object("app-menu"))
+
+    def do_activate(self):
+        # We only allow a single window and raise any existing ones
+        if not self.window:
+            # Windows are associated with the application
+            # when the last one is closed the application shuts down
+            self.window = ToyIdeWindow(application=self, title="Main Window")
+
+        self.window.present()
+
+    def do_command_line(self, command_line):
+        options = command_line.get_options_dict()
+        # convert GVariantDict -> GVariant -> dict
+        options = options.end().unpack()
+
+        if "test" in options:
+            # This is printed on the main instance
+            print("Test argument recieved: %s" % options["test"])
+
+        self.activate()
+        return 0
+
+    def on_about(self, action, param):
+        about_dialog = Gtk.AboutDialog(transient_for=self.window, modal=True)
+        about_dialog.present()
+
+    def on_quit(self, action, param):
+        self.quit()
+
+if __name__ == "__main__":
+    app = ToyIdeApp()
+    app.run(sys.argv)
diff --git a/gcc/toplev.cc b/gcc/toplev.cc
index 682459220a2..e0e1d7ba382 100644
--- a/gcc/toplev.cc
+++ b/gcc/toplev.cc
@@ -22,6 +22,9 @@ along with GCC; see the file COPYING3.  If not see
    in the proper order, and counts the time used by each.
    Error messages and low-level interface to malloc also handled here.  */
 
+#define INCLUDE_LIST
+#define INCLUDE_MAP
+#define INCLUDE_STRING
 #define INCLUDE_VECTOR
 #include "config.h"
 #include "system.h"
@@ -97,6 +100,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "dbgcnt.h"
 #include "gcc-urlifier.h"
 #include "unique-argv.h"
+#include "lsp/gcc-impl/toplev.h"
 
 #include "selftest.h"
 
@@ -451,12 +455,20 @@ compile_file (void)
   timevar_start (TV_PHASE_PARSING);
   timevar_push (TV_PARSE_GLOBAL);
 
+  if (flag_lsp_server)
+    gcc_lsp_server_serve_forever ();
+  
   /* Parse entire file and generate initial debug information.  */
   lang_hooks.parse_file ();
 
   timevar_pop (TV_PARSE_GLOBAL);
   timevar_stop (TV_PHASE_PARSING);
 
+#if 0
+  if (flag_lsp_worker)
+    gcc_lsp_worker_serve_forever ();
+#endif
+
   if (flag_dump_locations)
     dump_location_info (stderr);
 
@@ -2373,6 +2385,16 @@ toplev::main (int argc, char **argv)
   /* Exit early if we can (e.g. -help).  */
   if (!exit_after_options)
     {
+      if (flag_lsp_worker)
+	{
+	  main_input_filename = gcc_lsp_worker_init ();
+
+	  // FIXME: from opts-global.cc: add_input_filename:
+	  num_in_fnames++;
+	  in_fnames = XRESIZEVEC (const char *, in_fnames, num_in_fnames);
+	  in_fnames[num_in_fnames - 1] = main_input_filename;
+	}
+     
       /* Just in case lang_hooks.post_options ends up calling a debug_hook.
 	 This can happen with incorrect pre-processed input. */
       debug_hooks = &do_nothing_debug_hooks;
diff --git a/gcc/uris.cc b/gcc/uris.cc
new file mode 100644
index 00000000000..70112ecf44c
--- /dev/null
+++ b/gcc/uris.cc
@@ -0,0 +1,180 @@
+/* 
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#define INCLUDE_ALGORITHM
+#define INCLUDE_STRING
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "uris.h"
+#include "selftest.h"
+
+/* RFC 8089:  The "file" URI Scheme.  */
+
+uris::file_uri::file_uri (const char *local_path)
+: m_auth_path (nullptr),
+  m_path_absolute (local_path)
+{
+  // FIXME: make local_path absolute
+  gcc_assert (m_path_absolute[0] == '/');
+
+  // FIXME: any normalization needed?
+}
+
+bool
+uris::file_uri::local_p () const
+{
+  if (!m_auth_path)
+    return true;
+  if (*m_auth_path == "")
+    return true;
+  if (*m_auth_path == "localhost")
+    return true;
+  return false;    
+}
+
+std::string
+uris::file_uri::to_string () const
+{
+  std::string result ("file://");
+  if (m_auth_path)
+    result.append (*m_auth_path);
+  result.append (m_path_absolute);
+  return result;
+}
+
+std::unique_ptr<uris::file_uri>
+uris::parse_as_file_uri (const std::string &str)
+{
+  /* Must start with "file:" */
+  if (str.size () < 5)
+    return nullptr;
+  if (strncmp (str.c_str (), "file:", 5))
+    return nullptr;
+
+  /* file-hier-part is either:
+       "//" auth-path
+     or:
+       local-path
+  */
+
+  file_uri result;
+  std::string::const_iterator start_of_abs_path = str.begin () + 5;
+  if (str.size () >= 7)
+    if (str[5] == '/' && str[6] == '/')
+      {
+	std::string::const_iterator start_of_auth_path = start_of_abs_path + 2;
+	std::string::const_iterator end_of_auth_path
+	  = std::find (start_of_auth_path, str.end (), '/');
+	result.m_auth_path
+	  = std::make_unique<std::string> (start_of_auth_path, end_of_auth_path);
+	start_of_abs_path = end_of_auth_path;
+      }
+  result.m_path_absolute = std::string (start_of_abs_path, str.end ());
+  return std::make_unique<file_uri> (std::move (result));
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+void
+test_parse_as_file_uri_empty ()
+{
+  ASSERT_EQ (uris::parse_as_file_uri (""), nullptr);
+  // FIXME: error handling
+}
+
+void
+test_parse_as_file_uri_not_a_file ()
+{
+  ASSERT_EQ (uris::parse_as_file_uri ("http://www.example.com"), nullptr);
+  // FIXME: error handling
+}
+
+void
+test_parse_as_file_uri_local_no_authority ()
+{
+  auto uri = uris::parse_as_file_uri ("file:/path/to/file");
+  ASSERT_NE (uri, nullptr);
+  ASSERT_STREQ (uri->m_path_absolute.c_str (), "/path/to/file");
+  ASSERT_EQ (uri->m_auth_path, nullptr);
+  ASSERT_TRUE (uri->local_p ());
+}
+
+void
+test_parse_as_file_uri_local_empty_authority ()
+{
+  auto uri = uris::parse_as_file_uri ("file:///path/to/file");
+  ASSERT_NE (uri, nullptr);
+  ASSERT_STREQ (uri->m_path_absolute.c_str (), "/path/to/file");
+  ASSERT_NE (uri->m_auth_path, nullptr);
+  ASSERT_STREQ (uri->m_auth_path->c_str (), "");
+  ASSERT_TRUE (uri->local_p ());
+}
+
+void
+test_parse_as_file_uri_localhost ()
+{
+  auto uri = uris::parse_as_file_uri ("file://localhost/path/to/file");
+  ASSERT_NE (uri, nullptr);
+  ASSERT_STREQ (uri->m_path_absolute.c_str (), "/path/to/file");
+  ASSERT_NE (uri->m_auth_path, nullptr);
+  ASSERT_STREQ (uri->m_auth_path->c_str (), "localhost");
+  ASSERT_TRUE (uri->local_p ());
+}
+
+void
+test_parse_as_file_uri_nonlocal_explicit_authority ()
+{
+  auto uri = uris::parse_as_file_uri ("file://host.example.com/path/to/file");
+  ASSERT_NE (uri, nullptr);
+  ASSERT_STREQ (uri->m_path_absolute.c_str (), "/path/to/file");
+  ASSERT_NE (uri->m_auth_path, nullptr);
+  ASSERT_STREQ (uri->m_auth_path->c_str (), "host.example.com");
+  ASSERT_FALSE (uri->local_p ());
+}
+
+void
+test_file_uri_to_string_local ()
+{
+  uris::file_uri uri ("/path/to/file");
+  ASSERT_STREQ (uri.to_string ().c_str (), "file:///path/to/file");  
+}
+  
+/* Run all of the selftests within this file.  */
+
+void
+uris_cc_tests ()
+{
+  test_parse_as_file_uri_empty ();
+  test_parse_as_file_uri_not_a_file ();
+  test_parse_as_file_uri_local_no_authority ();
+  test_parse_as_file_uri_local_empty_authority ();
+  test_parse_as_file_uri_localhost ();
+  test_parse_as_file_uri_nonlocal_explicit_authority ();
+
+  test_file_uri_to_string_local ();
+  // FIXME: support for authority
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/uris.h b/gcc/uris.h
new file mode 100644
index 00000000000..88edc48f41e
--- /dev/null
+++ b/gcc/uris.h
@@ -0,0 +1,46 @@
+/* FIXME.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_URIS_H
+#define GCC_URIS_H
+
+namespace uris {
+
+/* RFC 8089:  The "file" URI Scheme.  */
+
+struct file_uri
+{
+  file_uri () = default;
+  file_uri (const char *local_path);
+
+  bool local_p () const;
+  std::string to_string () const;
+
+  std::unique_ptr<std::string> m_auth_path;
+  std::string m_path_absolute;
+};
+
+extern std::unique_ptr<file_uri>
+parse_as_file_uri (const std::string &str);
+  
+} // namespace uris
+
+
+#endif  /* GCC_URIS_H  */
diff --git a/libcpp/files.cc b/libcpp/files.cc
index 9234ead0d23..588b14c4a5a 100644
--- a/libcpp/files.cc
+++ b/libcpp/files.cc
@@ -2884,3 +2884,31 @@ cpp_get_converted_source (const char *fname, const char *input_charset)
   res.len = file.st.st_size;
   return res;
 }
+
+void
+cpp_reader_override_file_contents (struct cpp_reader *pfile,
+				   const char *abspath,
+				   const char *utf8_text)
+{
+  // Ensure we have a cache entry for ABSPATH
+  void **hash_slot
+    = htab_find_slot_with_hash (pfile->file_hash, abspath,
+				htab_hash_string (abspath), INSERT);
+  gcc_assert (hash_slot);
+
+  _cpp_file *file = make_cpp_file (nullptr, abspath);
+  file->path = xstrdup (abspath);
+  file->buffer_start = (uchar *)xstrdup (utf8_text);
+  file->buffer = file->buffer_start;
+  file->st.st_size = strlen (utf8_text);
+  file->buffer_valid = true;
+
+  cpp_file_hash_entry *entry
+    = new_file_hash_entry (pfile);
+  entry->next = (struct cpp_file_hash_entry *) *hash_slot;
+
+  entry->start_dir = &pfile->no_search_path;
+  entry->location = 0;
+  entry->u.file = file;
+  *hash_slot = (void *) entry;
+}
diff --git a/libcpp/include/cpplib.h b/libcpp/include/cpplib.h
index bc0d7714e96..32980b5ff49 100644
--- a/libcpp/include/cpplib.h
+++ b/libcpp/include/cpplib.h
@@ -1578,6 +1578,10 @@ extern cpp_buffer *cpp_get_buffer (cpp_reader *);
 extern struct _cpp_file *cpp_get_file (cpp_buffer *);
 extern cpp_buffer *cpp_get_prev (cpp_buffer *);
 extern void cpp_clear_file_cache (cpp_reader *);
+extern void cpp_reader_override_file_contents (struct cpp_reader *,
+					       const char *abspath,
+					       const char *utf8_text);
+
 
 /* cpp_get_converted_source returns the contents of the given file, as it exists
    after cpplib has read it and converted it from the input charset to the
-- 
2.49.0

