Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions lsp/ParserCLI.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package sigmastate.lsp

import sigmastate.lang.SigmaParser
import fastparse._
import scala.io.Source
import java.io.{PrintWriter, StringWriter}

/** Simple CLI wrapper around SigmaParser for LSP integration.
* Reads ErgoScript source from stdin or file, parses it, and outputs diagnostics as JSON.
*/
object ParserCLI {

case class Diagnostic(
line: Int,
column: Int,
message: String,
severity: String // "error" or "warning"
)

def parseSource(source: String): Either[List[Diagnostic], String] = {
try {
// Try to parse as expression
val exprResult = fastparse.parse(source, SigmaParser.Expr(_))
exprResult match {
case Parsed.Success(value, _) =>
Right(s"Parsed successfully: ${value.getClass.getSimpleName}")

case f @ Parsed.Failure(label, index, extra) =>
val pos = getLineCol(source, index)
val diag = Diagnostic(
line = pos._1,
column = pos._2,
message = s"Parse error: expected $label at ${extra.trace().msg}",
severity = "error"
)
Left(List(diag))
}
} catch {
case e: Exception =>
val diag = Diagnostic(
line = 0,
column = 0,
message = s"Parser exception: ${e.getMessage}",
severity = "error"
)
Left(List(diag))
}
}

def getLineCol(source: String, index: Int): (Int, Int) = {
val lines = source.take(index).split("\n", -1)
val line = lines.length - 1
val col = if (lines.isEmpty) 0 else lines.last.length
(line, col)
}

def diagnosticsToJSON(diags: List[Diagnostic]): String = {
val diagStrings = diags.map { d =>
s"""{"line":${d.line},"column":${d.column},"message":"${escapeJSON(d.message)}","severity":"${d.severity}"}"""
}
s"""{"diagnostics":[${diagStrings.mkString(",")}]}"""
}

def escapeJSON(s: String): String = {
s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
}

def main(args: Array[String]): Unit = {
val source = if (args.isEmpty) {
// Read from stdin
Source.fromInputStream(System.in).mkString
} else {
// Read from file
Source.fromFile(args(0)).mkString
}

parseSource(source) match {
case Left(diagnostics) =>
println(diagnosticsToJSON(diagnostics))
System.exit(1)

case Right(success) =>
println(s"""{"diagnostics":[],"success":"${escapeJSON(success)}"}""")
System.exit(0)
}
}
}
96 changes: 96 additions & 0 deletions lsp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# ErgoScript Minimal LSP (Proof-of-Concept)

This folder contains a minimal, low-cost Language Server Protocol (LSP) implementation for ErgoScript. It is intended as a hackathon proof-of-concept to provide basic IDE features to support development and AI agents: diagnostics, hover, and document symbols.

## What it provides

- **Real parser diagnostics**: When the Scala parser is available, calls `SigmaParser` via a CLI wrapper to get accurate parse errors with line/column information.
- **Fallback regex diagnostics**: If the Scala parser is unavailable, uses simple pattern matching for unmatched braces/parentheses and `TODO` markers.
- **Document symbols**: Extracts `val`, `def`, `let`, `func`, `type` symbols via regex.
- **Hover**: Shows the token under cursor with a small help message.

This is intentionally minimal to be low-effort and useful for tooling integration, with a clear upgrade path to full parser integration.

## Quick start

1. **Build the parser** (optional but recommended for real diagnostics):
```bash
# from repository root
sbt "parsers/publishLocal"
```

2. **Install Node.js dependencies and run server**:
```bash
cd lsp
npm install
npm start
```

The server automatically detects if the Scala parser JAR is available and uses it; otherwise it falls back to regex-based validation.

## How to use in VS Code (manual demo)

You can test the server with any LSP client that supports connecting to a stdio language server. For VS Code, the easiest path is to use the "LSP Client" extension or the "vscode-languageclient" example extension and point it to this server's `node server.js` process.

### Example (using `node` only, advanced)

The server speaks LSP over stdio. If you have an LSP client that can attach to a stdio server you can configure it to start `node /path/to/lsp/server.js` as the server command.

## Example file

Create `example.es` with some ErgoScript content and open it in the editor connected to this server. The server will emit diagnostics for unmatched braces and provide symbol extraction for lines beginning with `val`, `def`, `let`, `func`, or `type`.

## Architecture

```
┌─────────────────┐
│ LSP Client │ (VS Code, Neovim, etc.)
│ (stdio) │
└────────┬────────┘
│ LSP protocol
v
┌─────────────────┐
│ server.js │ Node.js LSP server
│ (stdio) │ • Hover, symbols, diagnostics
└────────┬────────┘
├─────────────────┐
│ │
v v
┌─────────────────┐ ┌─────────────────┐
│ ParserCLI.scala │ │ Regex fallback │
│ (via scala CLI) │ │ (basic checks) │
│ Uses SigmaParser│ └─────────────────┘
└─────────────────┘
v
┌─────────────────┐
│ SigmaParser │ Actual ErgoScript parser
│ (Scala/fastparse)
└─────────────────┘
```

When the Scala parser JAR is built (`sbt parsers/publishLocal`), the LSP server invokes `ParserCLI.scala` via the `scala` command to get real parse errors. Otherwise, it falls back to simple regex validation.

## Limitations and next steps

- **Parser invocation overhead**: Currently spawns a new `scala` process per validation. For production, consider:
- Running a persistent Scala server (e.g., via HTTP or Unix socket) to avoid startup costs.
- Compiling `ParserCLI.scala` to a native binary with Scala Native or GraalVM.
- **Limited symbol resolution**: Document symbols use regex, not the actual AST. Future work should query the parser's symbol table.
- Next steps to improve quality and LSP features:
- Add completions and signature help by reusing interpreter AST.
- Provide workspace/x references and find-definition using interpreter symbols.
- Optionally publish as an npm package and provide a VS Code extension to simplify usage.

## Files

- `server.js` - LSP server (stdio) with auto-detection of Scala parser
- `ParserCLI.scala` - Scala wrapper for SigmaParser that outputs JSON diagnostics
- `package.json` - Node package manifest
- `README.md` - this file
- `example.es` - sample ErgoScript file for testing

## License

Same as the repository (see LICENSE)
14 changes: 14 additions & 0 deletions lsp/example.es
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Simple ErgoScript example: a basic box guard script
// This validates that the output box has a specific value

val minValue = 1000000000L // 1 ERG in nanoERG

val outputOK = {
val out = OUTPUTS(0)
out.value >= minValue &&
out.propositionBytes == SELF.propositionBytes
}

sigmaProp(outputOK)
}
12 changes: 12 additions & 0 deletions lsp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "ergoscript-lsp",
"version": "0.1.0",
"description": "Minimal ErgoScript Language Server (proof-of-concept)",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"vscode-languageserver": "^8.0.0"
}
}
Loading