feat(lsp): hexa-native port of n6_lsp.py (PoC) - #4
Merged
Conversation
Port the n6 LSP server from Python to hexa-lang (dancinlab g1
hexa-native). New lsp/n6_lsp.hexa alongside the kept .py. Inline
LSP stdin/stdout framing (read_stdin_n_c + from_cstring), hand-rolled
HDR/KV checks (no re), substring JSON extraction (no json_parse dep),
docs map via #{} + has_key (no null literal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oads) Real LSP clients and Python json.dumps emit "method": "x" (space after the colon); the initial compact-only substring matcher missed those. Add value_start() (skips whitespace around the colon) + read_string_at() and route all five extractors through them. Now matches the .py --check output byte-for-byte on a mixed sample. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Hexa-native port of the n6 LSP server (
lsp/n6_lsp.py→ newlsp/n6_lsp.hexa), per dancinlab governance g1 (hexa-native). The.pyis KEPT — the~/.local/bin/n6-lspwrapper still points at it; repointing the wrapper to the.hexais a separate follow-up (see below). PoC to be replicated for hxc + kosmos.Standalone (does NOT
usehexa-lang'sself/lsp): the LSP stdin/stdout framing is implemented INLINE, copying the shape from hexa-lang main'sself/lsp/protocol.hexa/server.hexa(PR #1163). Ported features: TYPES/EDGES maps,validate_n6(text)diagnostics,hover(line),read_message()/write_message()framing, and theserve()dispatch loop (initialize / shutdown / exit / didOpen / didChange / didClose / hover).Verify (built + run via hexa-lang main
hexa-fast, all localPOOL_DISABLE=1)Test 1 — framing + loop (two framed messages → two framed responses + clean exit)
Input:
initializethenshutdown.od -cof stdout:EXIT: 0 (clean EOF exit). Two correctly framed responses;
hoverProvider:truepresent.Test 2 — diagnostics with a RAW newline in the text (byte-exact framing proof)
Input:
didOpenwithtext=@Z foo\n bogus continuation(the\nis a real newline inside the JSON string). Output frameContent-Length: 688and the 688-byte body is:{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///t.n6","diagnostics":[ {"range":{"start":{"line":0,"character":1},"end":{"line":0,"character":2}},"severity":1,"source":"n6-lsp","message":"unknown entry type @Z (alphabet: @P @C @L @F @R @S @X @? @E)"}, {"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":6}},"severity":1,"source":"n6-lsp","message":"malformed header — expected `@<type> <id>[ = <expr>] :: <domain> [<grade>]`"}, {"range":{"start":{"line":1,"character":2},"end":{"line":1,"character":20}},"severity":2,"source":"n6-lsp","message":"unrecognised continuation — expected an edge (<- -> => == ~> |> !!) or \"prose\""} ]}}The raw
\nwas decoded to a real newline and split into line 0 / line 1 — the reader did NOT stop at the newline (Content-Length 688 = full body). LEN matches.Parity cross-check (
.py --checkvs.hexa didOpen, identical mixed sample).pyand.hexaproduce the SAME four diagnostics (unknown @z, malformed header, unrecognised continuation [hint/sev2], continuation-not-2-space [sev1]) on the same file. Hover also verified end-to-end: a valid@F gravity :: physics [C]doc yields empty diagnostics, hover on the header →**@F** — Formula — explicit functional form, hover on<- mass→**<-** — depends_on edge.Hexa-porting gotchas (for the hxc/kosmos replication)
read_stdin_n_creturns achar*AS AN INT (a pointer). Declareextern fn read_stdin_n_c(n: int) -> intand wrap withfrom_cstring(read_stdin_n_c(N)). A-> strreturn does NOT auto-convert — it hands back the raw pointer integer.nullliteral — the parser rejects it. The docs map (uri→text) is built with#{}; membership ishas_key(docs, uri), not!= null. The LSPresult: nullis just the literal JSON stringnullbaked into the response body.re— the two.pyregexes (HDR,KV) are hand-rolled with string ops. HDR:starts_with("@")+ known type letter at[1]+ whitespace after it + contains::+ a[after::with a non-empty domain token + trimmed lineends_with("]"). KV:index_of("=") > 0, key (substring before=,trim_end) non-empty and contains no space/tab.hexa-fastmis-handlesnullin its fast path — another reason to keepnullout of the.hexasource entirely.protocol.hexaextractors match the COMPACT form"method":"x"only. Pythonjson.dumps(and many real clients) emit"method": "x"(space after the colon). The naive matcher silently missed those and fell through to the unknown-method branch. Fix shipped here:value_start(msg, name)finds"<name>", skips whitespace around the:, returns the value index;read_string_atdecodes from there. Replication note: copy these two helpers, do NOT reuse protocol.hexa's space-sensitive extractors verbatim.print→write(2)), no flush needed — matches the.pyflush calls' effect.Fidelity gaps vs the
.py.pyemitsbyte-canonical: UTF-8 with no BOMwhen text starts with. Skipped here (encoding-fragile in hexa). The CRLF check (byte-canonical: LF line endings only, viatext.contains("\r")) is KEPT and verified.--checkone-shot CLI mode not ported. The.hexais the stdio LSP server only (the wrapper invokes it that way).--check FILE(read a file, print lint, exit 1 on error) can be added when the wrapper is repointed."text", not the LAST. The.pytakescontentChanges[-1].text. UndertextDocumentSync: 1(full sync — what this server advertises) clients send exactly one change, so first == last. Equivalent in practice for this sync mode.Follow-up (NOT in this PR)
~/.local/bin/n6-lspfrom the.pyto the built.hexa(separate change; the.pystays as the canonical reference until then).--checkmode once the encoding/CLI surface is settled.