aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--README.org5
-rw-r--r--bin/dune3
-rw-r--r--bin/lsp.ml166
-rw-r--r--bin/main.ml4
-rw-r--r--docs/tooling.org54
-rw-r--r--dune-project1
-rw-r--r--editors/emacs/sol-mode.el319
-rw-r--r--flake.nix2
-rw-r--r--lib/front.ml29
-rw-r--r--lib/front.mli15
-rw-r--r--sol.opam2
12 files changed, 589 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
index c7c6422..82ef7ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ result
*.vo
*.glob
*.aux
+*.elc
diff --git a/README.org b/README.org
index 0592a66..540513c 100644
--- a/README.org
+++ b/README.org
@@ -200,6 +200,11 @@ file.sol=. Worked examples live in =examples/= (see
(structural and lexicographic decrease, extracted into admission;
the interval characterizations behind propagation).
+Tooling: =sol lsp= is a language server publishing admission
+diagnostics (totality, schemas, integrity) as you type;
+=editors/emacs/sol-mode.el= provides highlighting, navigation, an
+inferior REPL and eglot wiring. See =docs/tooling.org=.
+
Machine checked proofs live in =theories/=: the admission checker's
core claim, that strict containment decreases a well founded
measure, is proven in Rocq and compiled as part of the default
diff --git a/bin/dune b/bin/dune
index 1ad9289..049ef66 100644
--- a/bin/dune
+++ b/bin/dune
@@ -1,4 +1,5 @@
(executable
(name main)
(public_name sol)
- (libraries sol))
+ (modules main lsp)
+ (libraries sol yojson))
diff --git a/bin/lsp.ml b/bin/lsp.ml
new file mode 100644
index 0000000..bfe2573
--- /dev/null
+++ b/bin/lsp.ml
@@ -0,0 +1,166 @@
+(* The Sol language server: JSON-RPC over stdio, hand rolled on
+ yojson. One capability, the load-bearing one: diagnostics. Every
+ edit runs the same admission the runtime runs, so a rule with no
+ decrease witness or a fact violating its schema is an error in
+ the editor, at typing time, with the same message the loader
+ gives. Parse errors carry positions; admission errors anchor at
+ the top of the file until the loader learns locations. *)
+
+let log_json json = Yojson.Safe.to_string json
+
+let send json =
+ let body = log_json json in
+ Printf.printf "Content-Length: %d\r\n\r\n%s" (String.length body) body;
+ flush stdout
+
+let read_message () =
+ let rec headers length =
+ match In_channel.input_line stdin with
+ | None -> None
+ | Some line -> (
+ let line = String.trim line in
+ if line = "" then length
+ else
+ match String.index_opt line ':' with
+ | Some i
+ when String.lowercase_ascii (String.sub line 0 i) = "content-length"
+ ->
+ headers
+ (int_of_string_opt
+ (String.trim
+ (String.sub line (i + 1) (String.length line - i - 1))))
+ | _ -> headers length)
+ in
+ match headers None with
+ | None -> None
+ | Some length -> (
+ match really_input_string stdin length with
+ | body -> ( try Some (Yojson.Safe.from_string body) with _ -> None)
+ | exception End_of_file -> None)
+
+let member name json =
+ match json with
+ | `Assoc fields -> Option.value ~default:`Null (List.assoc_opt name fields)
+ | _ -> `Null
+
+let to_string_opt = function `String s -> Some s | _ -> None
+
+(* Everything the checker can say about a document, as LSP
+ diagnostics: (line, column, message), zero-based lines. *)
+let diagnostics text =
+ match Sol.Front.parse text with
+ | Error { Sol.Front.line; column; message } ->
+ [ (max 0 (line - 1), max 0 column, message) ]
+ | Ok statements -> (
+ match Sol.Program.load statements with
+ | Error error -> [ (0, 0, Format.asprintf "%a" Sol.Program.pp_error error) ]
+ | Ok _ -> [])
+
+let publish uri text =
+ let items =
+ List.map
+ (fun (line, column, message) ->
+ `Assoc
+ [
+ ( "range",
+ `Assoc
+ [
+ ( "start",
+ `Assoc [ ("line", `Int line); ("character", `Int column) ]
+ );
+ ( "end",
+ `Assoc
+ [ ("line", `Int line); ("character", `Int (column + 1)) ]
+ );
+ ] );
+ ("severity", `Int 1);
+ ("source", `String "sol");
+ ("message", `String message);
+ ])
+ (diagnostics text)
+ in
+ send
+ (`Assoc
+ [
+ ("jsonrpc", `String "2.0");
+ ("method", `String "textDocument/publishDiagnostics");
+ ("params", `Assoc [ ("uri", `String uri); ("diagnostics", `List items) ]);
+ ])
+
+let respond id result =
+ send (`Assoc [ ("jsonrpc", `String "2.0"); ("id", id); ("result", result) ])
+
+let check_notification params =
+ let uri =
+ to_string_opt (member "uri" (member "textDocument" params))
+ in
+ let text =
+ match to_string_opt (member "text" (member "textDocument" params)) with
+ | Some text -> Some text
+ | None -> (
+ (* didChange, full sync: the last full content change. *)
+ match member "contentChanges" params with
+ | `List changes -> (
+ match List.rev changes with
+ | last :: _ -> to_string_opt (member "text" last)
+ | [] -> None)
+ | _ -> None)
+ in
+ match (uri, text) with Some uri, Some text -> publish uri text | _ -> ()
+
+let run () =
+ set_binary_mode_in stdin true;
+ set_binary_mode_out stdout true;
+ let running = ref true in
+ while !running do
+ match read_message () with
+ | None -> running := false
+ | Some message -> (
+ let id = member "id" message in
+ let params = member "params" message in
+ match to_string_opt (member "method" message) with
+ | Some "initialize" ->
+ respond id
+ (`Assoc
+ [
+ ( "capabilities",
+ `Assoc [ ("textDocumentSync", `Int 1) ] );
+ ( "serverInfo",
+ `Assoc
+ [
+ ("name", `String "sol-lsp"); ("version", `String "0.1.0");
+ ] );
+ ])
+ | Some "shutdown" -> respond id `Null
+ | Some "exit" -> running := false
+ | Some ("textDocument/didOpen" | "textDocument/didChange") ->
+ check_notification params
+ | Some "textDocument/didClose" -> (
+ match to_string_opt (member "uri" (member "textDocument" params)) with
+ | Some uri ->
+ send
+ (`Assoc
+ [
+ ("jsonrpc", `String "2.0");
+ ("method", `String "textDocument/publishDiagnostics");
+ ( "params",
+ `Assoc
+ [ ("uri", `String uri); ("diagnostics", `List []) ] );
+ ])
+ | None -> ())
+ | Some _ when id <> `Null ->
+ (* An unsupported request deserves an answer, not silence. *)
+ send
+ (`Assoc
+ [
+ ("jsonrpc", `String "2.0");
+ ("id", id);
+ ( "error",
+ `Assoc
+ [
+ ("code", `Int (-32601));
+ ("message", `String "method not found");
+ ] );
+ ])
+ | _ -> ())
+ done
diff --git a/bin/main.ml b/bin/main.ml
index c5c329a..eb212d2 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -48,6 +48,7 @@ let repl () =
print_endline "Sol. Statements accumulate; Query expression: ... runs.";
let rec loop history =
print_string "sol> ";
+ flush stdout;
match In_channel.input_line stdin with
| None -> ()
| Some line when String.trim line = "" -> loop history
@@ -73,7 +74,8 @@ let repl () =
let () =
match Sys.argv with
| [| _ |] -> repl ()
+ | [| _; "lsp" |] -> Lsp.run ()
| [| _; path |] -> run_file path
| _ ->
- prerr_endline "usage: sol [file.sol]";
+ prerr_endline "usage: sol [file.sol | lsp]";
exit 1
diff --git a/docs/tooling.org b/docs/tooling.org
new file mode 100644
index 0000000..7fb0e8c
--- /dev/null
+++ b/docs/tooling.org
@@ -0,0 +1,54 @@
+#+TITLE: Tooling
+
+* About
+
+This document describes the developer tooling around Sol: the
+language server, the Emacs mode, and the REPL. The tooling reuses
+the pipeline instead of imitating it; the checker the editor runs is
+the checker the runtime runs.
+
+* The language server
+
+=sol lsp= speaks the Language Server Protocol over stdio. It has one
+capability, the load bearing one: diagnostics. On every open and
+every change the document goes through the same parse and the same
+admission the loader performs, so a recursive rule without a
+decrease witness, a fact violating its schema, an unknown relation
+or a failed membership criterion appears in the editor at typing
+time, with the same message the runtime would give. Parse errors
+carry line and column; admission errors anchor at the top of the
+file until the loader learns source locations.
+
+The server is hand rolled JSON-RPC on yojson, in =bin/lsp.ml=. It
+answers =initialize=, tracks documents through full-content sync,
+publishes diagnostics, clears them on close, and answers unsupported
+requests with method-not-found instead of silence.
+
+* The Emacs mode
+
+=editors/emacs/sol-mode.el= provides:
+
+- highlighting per token category under the delimiter law:
+ directives, combinators, structural and generic labels, relation
+ heads, variables, references (=#Name=), literals;
+- =%= line comments, bracket pairing for =[]= ={}= =()=, prettify
+ of =:and= =:or= =:not= into their logical glyphs;
+- imenu over Relations, Rules and Queries, and defun navigation
+ across top level forms;
+- =run-sol= (C-c C-z), an inferior REPL through comint;
+ =sol-send-line= (C-c C-l) sends the statement at point;
+ =sol-run-buffer= (C-c C-k) runs the file and shows answers in a
+ compilation buffer;
+- eglot wiring: with eglot enabled, =sol lsp= starts automatically
+ for =sol-mode= buffers.
+
+Install by adding the directory to =load-path= and requiring
+=sol-mode=; =.sol= files open in the mode automatically.
+
+* The REPL
+
+=sol= with no arguments reads one statement per line. Declarations
+and facts accumulate; =Query expression: ...= runs against
+everything accumulated so far. A rejected statement reports its
+error and changes nothing. The prompt flushes eagerly so the REPL
+works under comint and pipes.
diff --git a/dune-project b/dune-project
index d490405..50c0e42 100644
--- a/dune-project
+++ b/dune-project
@@ -21,6 +21,7 @@
(ocaml
(>= 5.1))
zarith
+ yojson
(menhir :build)
(coq :build)
(alcotest :with-test)))
diff --git a/editors/emacs/sol-mode.el b/editors/emacs/sol-mode.el
new file mode 100644
index 0000000..f46c0dd
--- /dev/null
+++ b/editors/emacs/sol-mode.el
@@ -0,0 +1,319 @@
+;;; sol-mode.el --- Major mode for the Sol relational language -*- lexical-binding: t; -*-
+
+;; Author: Marcos Magueta
+;; Keywords: languages, logic, relational
+;; Version: 0.2.0
+;; Package-Requires: ((emacs "26.1"))
+
+;;; Commentary:
+
+;; A major mode for Sol, a total relational language with Smalltalk-like
+;; message syntax. It provides:
+;;
+;; - Syntax highlighting, one face per token category: directives,
+;; logical combinators, reserved labels, generic labels, relation
+;; heads, variables, references (#Name), literals, delimiters.
+;; - Navigation: imenu over Relations, Rules and Queries;
+;; beginning/end-of-defun over top-level forms.
+;; - An inferior Sol REPL (`run-sol') and `sol-run-buffer' to execute
+;; the current file.
+;; - Eglot wiring: `sol lsp' serves diagnostics, so admission errors
+;; (totality, schemas, integrity) appear while typing.
+;;
+;; Installation:
+;;
+;; (add-to-list 'load-path "/path/to/sol2/editors/emacs")
+;; (require 'sol-mode)
+;;
+;; Files ending in .sol open in `sol-mode' automatically.
+
+;;; Code:
+
+(require 'comint)
+
+(defgroup sol nil
+ "Major mode for the Sol relational language."
+ :group 'languages
+ :prefix "sol-")
+
+(defcustom sol-indent-offset 2
+ "Number of columns to indent per bracket nesting level."
+ :type 'integer
+ :group 'sol)
+
+(defcustom sol-program "sol"
+ "Path to the sol executable, used by the REPL, the runner and eglot."
+ :type 'string
+ :group 'sol)
+
+;;; ----------------------------------------------------------------------
+;;; Faces -- one colour per token category.
+;;; ----------------------------------------------------------------------
+
+(defface sol-keyword-face
+ '((((background dark)) (:foreground "#c678dd" :weight bold))
+ (((background light)) (:foreground "#a626a4" :weight bold)))
+ "Face for top-level directives: Relation, Rule, Query."
+ :group 'sol)
+
+(defface sol-combinator-face
+ '((((background dark)) (:foreground "#56b6c2" :weight bold))
+ (((background light)) (:foreground "#0184bc" :weight bold)))
+ "Face for the logical combinators :and, :or and :not."
+ :group 'sol)
+
+(defface sol-builtin-label-face
+ '((((background dark)) (:foreground "#e5c07b"))
+ (((background light)) (:foreground "#b07d00")))
+ "Face for structural labels: named:, schema:, head:, body:, and kin."
+ :group 'sol)
+
+(defface sol-label-face
+ '((((background dark)) (:foreground "#61afef"))
+ (((background light)) (:foreground "#4078f2")))
+ "Face for generic attribute labels, e.g. value:, left:, age:."
+ :group 'sol)
+
+(defface sol-relation-face
+ '((((background dark)) (:foreground "#98c379" :weight bold))
+ (((background light)) (:foreground "#50a14f" :weight bold)))
+ "Face for relation heads -- the first name after `['."
+ :group 'sol)
+
+(defface sol-variable-face
+ '((((background dark)) (:foreground "#e06c75"))
+ (((background light)) (:foreground "#e45649")))
+ "Face for logical variables -- capitalised identifiers in term position."
+ :group 'sol)
+
+(defface sol-reference-face
+ '((((background dark)) (:foreground "#d19a66" :weight bold))
+ (((background light)) (:foreground "#986801" :weight bold)))
+ "Face for relation references, #Name."
+ :group 'sol)
+
+(defface sol-number-face
+ '((((background dark)) (:foreground "#ff79c6"))
+ (((background light)) (:foreground "#c41a8d")))
+ "Face for integer and rational literals."
+ :group 'sol)
+
+(defface sol-bracket-face
+ '((((background dark)) (:foreground "#828997"))
+ (((background light)) (:foreground "#9ca0a4")))
+ "Face for delimiters and the cons bar |."
+ :group 'sol)
+
+;;; ----------------------------------------------------------------------
+;;; Syntax table.
+;;; ----------------------------------------------------------------------
+
+(defvar sol-mode-syntax-table
+ (let ((table (make-syntax-table)))
+ ;; Line comments start with % and run to end of line.
+ (modify-syntax-entry ?% "<" table)
+ (modify-syntax-entry ?\n ">" table)
+ ;; Strings.
+ (modify-syntax-entry ?\" "\"" table)
+ (modify-syntax-entry ?\\ "\\" table)
+ ;; The delimiter law: [] propositions, {} records, () grouping,
+ ;; #( ... ) sequences. All pair for navigation.
+ (modify-syntax-entry ?\[ "(]" table)
+ (modify-syntax-entry ?\] ")[" table)
+ (modify-syntax-entry ?\{ "(}" table)
+ (modify-syntax-entry ?\} "){" table)
+ (modify-syntax-entry ?\( "()" table)
+ (modify-syntax-entry ?\) ")(" table)
+ ;; # prefixes references and sequences.
+ (modify-syntax-entry ?# "'" table)
+ ;; Identifier constituents.
+ (modify-syntax-entry ?_ "_" table)
+ ;; Punctuation used as token glue.
+ (modify-syntax-entry ?: "." table)
+ (modify-syntax-entry ?| "." table)
+ (modify-syntax-entry ?/ "." table)
+ table)
+ "Syntax table for `sol-mode'.")
+
+;;; ----------------------------------------------------------------------
+;;; Font lock. Order matters: earlier entries win.
+;;; ----------------------------------------------------------------------
+
+(defconst sol-font-lock-keywords
+ (list
+ ;; 1. Top-level directives.
+ '("^[[:space:]]*\\(Relation\\|Rule\\|Query\\)\\_>" 1 'sol-keyword-face)
+ ;; 2. Logical combinators.
+ '("\\(:and\\|:or\\|:not\\)\\_>" 1 'sol-combinator-face)
+ ;; 3. Relation references, #Name.
+ '("\\(#[A-Za-z][A-Za-z0-9_]*\\)" 1 'sol-reference-face)
+ ;; 4. Structural labels.
+ '("\\_<\\(named\\|schema\\|name\\|head\\|body\\|expression\\|domain\\|variable\\|quantifier\\):"
+ 1 'sol-builtin-label-face)
+ ;; 5. Relation head: the first name after `['.
+ '("\\[[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1 'sol-relation-face)
+ ;; 6. Generic attribute labels.
+ '("\\_<\\([A-Za-z_][A-Za-z0-9_]*\\):" 1 'sol-label-face)
+ ;; 7. Logical variables: capitalised identifiers in term position.
+ '("\\_<\\([A-Z][A-Za-z0-9_]*\\)\\_>" 1 'sol-variable-face)
+ ;; 8. Numeric literals: integers and rationals.
+ '("\\_<-?[0-9]+\\(?:/[0-9]+\\)?\\_>" 0 'sol-number-face)
+ ;; 9. Delimiters.
+ '("[][}{|]" 0 'sol-bracket-face))
+ "Font-lock highlighting for `sol-mode'.")
+
+(defvar sol-prettify-symbols-alist
+ '((":and" . ?∧)
+ (":or" . ?∨)
+ (":not" . ?¬))
+ "Symbol prettification for `sol-mode'.")
+
+;;; ----------------------------------------------------------------------
+;;; Navigation.
+;;; ----------------------------------------------------------------------
+
+(defconst sol-defun-regexp
+ "^[[:space:]]*\\(?:Relation\\|Rule\\|Query\\)\\_>\\|^[[:space:]]*\\["
+ "Regexp matching the start of a top-level Sol form: a directive or a fact.")
+
+(defvar sol-imenu-generic-expression
+ (list
+ (list "Relation"
+ "\\_<Relation\\_>[[:space:]]+named:[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1)
+ (list "Rule"
+ "\\_<Rule\\_>[[:space:]]+name:[[:space:]]*\"\\([^\"]+\\)\"" 1)
+ (list "Query"
+ "\\_<Query\\_>[[:space:]]+expression:[[:space:]]*\\(?:(\\|\\[\\)[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1))
+ "Imenu configuration for `sol-mode'.")
+
+(defun sol-beginning-of-defun (&optional arg)
+ "Move backward to the start of a top-level Sol form.
+With ARG, repeat that many times. Returns non-nil on success."
+ (setq arg (or arg 1))
+ (let ((found nil))
+ (while (> arg 0)
+ (setq found (re-search-backward sol-defun-regexp nil t))
+ (setq arg (1- arg)))
+ (when found
+ (goto-char (match-beginning 0))
+ (back-to-indentation))
+ found))
+
+(defun sol-end-of-defun ()
+ "Move to the end of the current top-level Sol form."
+ (when (looking-at-p sol-defun-regexp)
+ (forward-char 1))
+ (if (re-search-forward sol-defun-regexp nil 'move)
+ (progn
+ (goto-char (match-beginning 0))
+ (skip-chars-backward " \t\n"))
+ (goto-char (point-max))))
+
+;;; ----------------------------------------------------------------------
+;;; Indentation: bracket-depth based, conservative.
+;;; ----------------------------------------------------------------------
+
+(defun sol-indent-line ()
+ "Indent the current line by its bracket nesting depth."
+ (let* ((bol (line-beginning-position))
+ (depth (car (syntax-ppss bol))))
+ (save-excursion
+ (back-to-indentation)
+ (when (and (> depth 0) (looking-at-p "[])}]"))
+ (setq depth (1- depth))))
+ (if (> depth 0)
+ (indent-line-to (* depth sol-indent-offset))
+ 'noindent)))
+
+;;; ----------------------------------------------------------------------
+;;; Inferior Sol: the REPL, through comint.
+;;; ----------------------------------------------------------------------
+
+(defvar inferior-sol-buffer "*sol*"
+ "Buffer name of the inferior Sol process.")
+
+(define-derived-mode inferior-sol-mode comint-mode "Inferior Sol"
+ "Major mode for the Sol REPL."
+ (setq comint-prompt-regexp "^sol> ")
+ (setq comint-prompt-read-only t))
+
+;;;###autoload
+(defun run-sol ()
+ "Start the Sol REPL, or pop to it if it is already running."
+ (interactive)
+ (unless (comint-check-proc inferior-sol-buffer)
+ (with-current-buffer (make-comint "sol" sol-program)
+ (inferior-sol-mode)))
+ (pop-to-buffer inferior-sol-buffer))
+
+(defun sol-send-line ()
+ "Send the current line (one statement) to the REPL."
+ (interactive)
+ (let ((statement (string-trim (or (thing-at-point 'line t) ""))))
+ (run-sol)
+ (with-current-buffer inferior-sol-buffer
+ (goto-char (point-max))
+ (insert statement)
+ (comint-send-input))))
+
+(defun sol-run-buffer ()
+ "Run the current file with sol, showing answers in a compilation buffer."
+ (interactive)
+ (when (buffer-modified-p) (save-buffer))
+ (compile (format "%s %s" sol-program (shell-quote-argument buffer-file-name))))
+
+;;; ----------------------------------------------------------------------
+;;; Eglot: diagnostics from `sol lsp'.
+;;; ----------------------------------------------------------------------
+
+(defvar eglot-server-programs)
+
+(with-eval-after-load 'eglot
+ (add-to-list 'eglot-server-programs
+ `(sol-mode . (,sol-program "lsp"))))
+
+;;; ----------------------------------------------------------------------
+;;; Keymap.
+;;; ----------------------------------------------------------------------
+
+(defvar sol-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-z") #'run-sol)
+ (define-key map (kbd "C-c C-l") #'sol-send-line)
+ (define-key map (kbd "C-c C-k") #'sol-run-buffer)
+ (define-key map (kbd "C-c C-j") #'imenu)
+ map)
+ "Keymap for `sol-mode'.")
+
+;;; ----------------------------------------------------------------------
+;;; Mode definition.
+;;; ----------------------------------------------------------------------
+
+;;;###autoload
+(define-derived-mode sol-mode prog-mode "Sol"
+ "Major mode for editing Sol relational programs.
+
+\\{sol-mode-map}"
+ :syntax-table sol-mode-syntax-table
+ (setq-local font-lock-defaults '(sol-font-lock-keywords))
+ ;; Comments.
+ (setq-local comment-start "% ")
+ (setq-local comment-end "")
+ (setq-local comment-start-skip "%+[ \t]*")
+ ;; Navigation.
+ (setq-local beginning-of-defun-function #'sol-beginning-of-defun)
+ (setq-local end-of-defun-function #'sol-end-of-defun)
+ (setq-local imenu-generic-expression sol-imenu-generic-expression)
+ (setq-local imenu-case-fold-search nil)
+ ;; Indentation.
+ (setq-local indent-line-function #'sol-indent-line)
+ (setq-local indent-tabs-mode nil)
+ ;; Eye-candy.
+ (setq-local prettify-symbols-alist sol-prettify-symbols-alist))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.sol\\'" . sol-mode))
+
+(provide 'sol-mode)
+
+;;; sol-mode.el ends here
diff --git a/flake.nix b/flake.nix
index bd1d2d2..4bf732e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -38,6 +38,7 @@
];
propagatedBuildInputs = [
ocamlPackages.zarith
+ ocamlPackages.yojson
];
checkInputs = [ ocamlPackages.alcotest ];
doCheck = true;
@@ -63,6 +64,7 @@
ocamlPackages.utop
ocamlPackages.odoc
ocamlPackages.zarith
+ ocamlPackages.yojson
ocamlPackages.alcotest
ocamlPackages.menhir
# Rocq, for the verified checkers and their extraction
diff --git a/lib/front.ml b/lib/front.ml
index 7cacc20..b728825 100644
--- a/lib/front.ml
+++ b/lib/front.ml
@@ -1,14 +1,27 @@
-let parse_string source =
+type error = { line : int; column : int; message : string }
+
+let position lexbuf =
+ let p = lexbuf.Lexing.lex_curr_p in
+ (p.Lexing.pos_lnum, p.Lexing.pos_cnum - p.Lexing.pos_bol)
+
+let parse source =
let lexbuf = Lexing.from_string source in
match Parser.program Lexer.token lexbuf with
| statements -> Ok statements
| exception Lexer.Error message ->
- let position = lexbuf.Lexing.lex_curr_p in
- Error
- (Printf.sprintf "lexical error at line %d: %s"
- position.Lexing.pos_lnum message)
+ let line, column = position lexbuf in
+ Error { line; column; message }
| exception Parser.Error ->
- let position = lexbuf.Lexing.lex_curr_p in
+ let line, column = position lexbuf in
Error
- (Printf.sprintf "syntax error at line %d, before %S"
- position.Lexing.pos_lnum (Lexing.lexeme lexbuf))
+ {
+ line;
+ column;
+ message = Printf.sprintf "syntax error before %S" (Lexing.lexeme lexbuf);
+ }
+
+let parse_string source =
+ match parse source with
+ | Ok statements -> Ok statements
+ | Error { line; message; _ } ->
+ Error (Printf.sprintf "line %d: %s" line message)
diff --git a/lib/front.mli b/lib/front.mli
index 00fa65f..a4e91e6 100644
--- a/lib/front.mli
+++ b/lib/front.mli
@@ -1,7 +1,18 @@
(** Front door: text to statements.
Wraps the generated lexer and parser so callers see one total
- function from source text to syntax or a positioned message.
- The REPL, the file runner, and the tests all enter here. *)
+ function from source text to syntax or a positioned error. The
+ REPL, the file runner, the LSP server, and the tests all enter
+ here. *)
+
+type error = {
+ line : int; (** 1-based, as editors count *)
+ column : int; (** 0-based character offset in the line *)
+ message : string;
+}
+
+val parse : string -> (Ast.statement list, error) result
val parse_string : string -> (Ast.statement list, string) result
+(** [parse] with the position folded into the message, for callers
+ that only print. *)
diff --git a/sol.opam b/sol.opam
index c2dc35f..fe4e55a 100644
--- a/sol.opam
+++ b/sol.opam
@@ -10,7 +10,9 @@ depends: [
"dune" {>= "3.17"}
"ocaml" {>= "5.1"}
"zarith"
+ "yojson"
"menhir" {build}
+ "coq" {build}
"alcotest" {with-test}
"odoc" {with-doc}
]