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
949 changes: 568 additions & 381 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
[package]
name = "ic10lsp"
version = "0.7.4"
version = "0.7.5"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.1.13", features = ["derive"] }
phf = {version="0.11.1", features=["macros"]}
phf = {version="0.13.1", features=["macros"]}
regex = "1.9.3"
serde_json = "1.0.94"
tokio = {version="1.26.0", features=["full"]}
tower-lsp = "0.19.0"
tree-sitter = "0.20.9"
tree-sitter-ic10 = "0.5.2"
tokio = {version="1.48.0", features=["full"]}
tower-lsp = "0.20.0"
tree-sitter = "0.25.7"
tree-sitter-ic10 = "0.6.2"

[build-dependencies]
phf_codegen = "0.11.1"
phf_codegen = "0.13.1"
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Xandaros

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
78 changes: 68 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,90 @@
# IC10LSP

A simple language server for the IC10 MIPS-like language in the game Stationeers.
A language server for IC10, the MIPS-like assembly language used in the game
Stationeers. Talks LSP over stdio or TCP, so it works with any LSP-capable
editor.

Features:
![Demo](demo.gif)

## Features

- Completions (not fully)
- Hover information
- Completions for instructions, defines/aliases/labels, logic types, slot
logic types, and batch modes (raw register/device literals and
reagent-mode names aren't completed)
- Hover information for instructions, defines/aliases/labels, and logic
types
- Signature help
- Goto definition
- Diagnostic information
- Document symbols (an outline of defines, aliases, and labels)
- Diagnostics: syntax errors, unsupported instructions, type mismatches,
wrong argument counts, duplicate definitions, line/column length limits,
and a few style lints
- Quick fixes for some of those lints
- Inlay hints resolving hashed Stationpedia item/logic-type literals to
their names
- Semantic highlighting

![Demo](demo.gif)
## Installation

Prebuilt binaries for Linux and Windows are attached to each
[GitHub release](https://github.com/Xandaros/ic10lsp/releases).

To build from source:

```sh
git clone https://github.com/Xandaros/ic10lsp.git
cd ic10lsp
cargo build --release
```

The binary ends up at `target/release/ic10lsp` (`ic10lsp.exe` on Windows).

## Usage

By default the server communicates over stdio, which is what most editors
expect when they spawn a language server themselves:

```sh
ic10lsp
```

Two TCP modes are also available, for setups that talk to a language server
over a socket instead:

```sh
ic10lsp --listen [host] [port] # bind and wait for one client (default 127.0.0.1:9257)
ic10lsp <host> <port> # connect out to a listening client
```

ic10lsp doesn't ship editor-specific plugins; point your editor's LSP client
at the binary (or the socket, in TCP mode) yourself.

## Configuration

The language server exposes the following configuration options:
The language server reads the following configuration options via
`workspace/didChangeConfiguration`:

| Key | Description | Default |
| --------------------------- | ------------------------------------------------ | ------- |
| max_lines | Maximum number of lines | 128 |
| max_columns | Maximum number of columns | 52 |
| max_columns | Maximum number of columns | 90 |
| warnings.overline_comment | Emit a warning on comments past the line limit | true |
| warnings.overcolumn_comment | Emit a warning on comments past the column limit | true |
| warnings.overcolumn_comment | Emit a warning on comments past the column limit | false |

## Commands

The language server exposes the following commands:
The language server exposes the following commands via
`workspace/executeCommand`:

| Command | Description |
| ------- | ------------------------------------------------------ |
| version | Show a message with the version of the language server |

## Related

- [tree-sitter-ic10](https://crates.io/crates/tree-sitter-ic10) -- the IC10
grammar this server parses with.

## License

MIT. See [LICENSE](LICENSE).
12 changes: 11 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
//! Generates `HASH_NAME_LOOKUP` and `HASH_NAMES` from `stationpedia.txt`.
//!
//! `stationpedia.txt` is a `<hash> <name>` line per Stationeers prefab, dumped from the game.
//! Baking it into `phf` maps at build time means the server can resolve a hashed item/logic-type
//! constant to its human-readable name (used for inlay hints and hash literal completion) without
//! parsing that file or hashing anything at runtime. The generated file is included directly into
//! `src/instructions.rs` via `include!`.

use std::{
env,
fs::{self, File},
Expand All @@ -21,7 +29,9 @@ fn main() {
let mut it = line.splitn(2, ' ');
let hash = it.next().unwrap();
let name = it.next().unwrap();
map_builder.entry(hash, &format!("\"{}\"", name));

let formatted_name = format!("\"{}\"", name);
map_builder.entry(hash, formatted_name);

if !check_set.contains(name) {
set_builder.entry(name);
Expand Down
8 changes: 8 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use clap::Parser;

/// Selects how the server exchanges LSP messages with the client.
///
/// The three transport modes share `host`/`port`, but interpret them
/// differently: with neither flag set the server talks over stdio (the
/// normal case, since editors spawn the server as a subprocess); `--listen`
/// binds a TCP socket on `host`:`port` and waits for one client to connect;
/// with `--listen` absent but `host`/`port` given, the server instead
/// connects out to that address as a TCP client.
#[derive(Parser, Debug)]
#[command(version)]
pub(crate) struct Cli {
Expand Down
58 changes: 57 additions & 1 deletion src/instructions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
//! The static IC10 instruction and logic-type database.
//!
//! Everything here is derived from the game itself (instruction argument
//! shapes, hover docs, valid `LogicType`/`SlotLogicType`/batch and reagent
//! mode names) and never changes at runtime, so it's all `const` `phf`
//! maps/sets. `main.rs` uses it for completion, signature help, hover, and
//! the type-checking pass in diagnostics. `HASH_NAME_LOOKUP` and
//! `HASH_NAMES` are pulled in at the bottom via `include!` from a table
//! generated by `build.rs` out of `stationpedia.txt`.

use std::fmt::Display;

use phf::{phf_map, phf_set};

/// A value kind an instruction operand can hold.
///
/// `Name` is distinct from an identifier reference: it marks the operand
/// position where `define`/`alias`/`label` introduce a new name, as opposed
/// to a position that reads one.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DataType {
Number,
Expand All @@ -14,9 +29,14 @@ pub(crate) enum DataType {
ReagentMode,
}

/// One operand's accepted type(s), e.g. `VALUE` accepts a register or a
/// number literal. Displays as `a|b` (parenthesized when more than one
/// alternative) to match the operand notation used in IC10 hover text.
#[derive(Debug)]
pub(crate) struct Union<'a>(pub(crate) &'a [DataType]);

/// An instruction's ordered parameter list, used both to type-check calls
/// and to render signature help.
#[derive(Debug)]
pub(crate) struct InstructionSignature(pub(crate) &'static [Union<'static>]);

Expand Down Expand Up @@ -154,7 +174,7 @@ pub(crate) const INSTRUCTIONS: phf::Map<&'static str, InstructionSignature> = ph
"lbns" => InstructionSignature(&[REGISTER, VALUE, VALUE, VALUE, SLOT_LOGIC_TYPE, BATCH_MODE]),
"lbs" => InstructionSignature(&[REGISTER, VALUE, VALUE, SLOT_LOGIC_TYPE, BATCH_MODE]),
"not" => InstructionSignature(&[REGISTER, VALUE]),
"sbn" => InstructionSignature(&[VALUE, VALUE, LOGIC_TYPE, REGISTER]),
"sbn" => InstructionSignature(&[VALUE, VALUE, LOGIC_TYPE, VALUE]),
"sbs" => InstructionSignature(&[VALUE, VALUE, SLOT_LOGIC_TYPE, REGISTER]),
"sla" => InstructionSignature(&[REGISTER, VALUE, VALUE]),
"sll" => InstructionSignature(&[REGISTER, VALUE, VALUE]),
Expand All @@ -163,6 +183,12 @@ pub(crate) const INSTRUCTIONS: phf::Map<&'static str, InstructionSignature> = ph
"snan" => InstructionSignature(&[REGISTER, VALUE]),
"snanz" => InstructionSignature(&[REGISTER, VALUE]),
"ss" => InstructionSignature(&[DEVICE, VALUE, SLOT_LOGIC_TYPE, REGISTER]),
"get" => InstructionSignature(&[REGISTER, DEVICE, VALUE]),
"getd" => InstructionSignature(&[REGISTER, VALUE, VALUE]),
"put" => InstructionSignature(&[DEVICE, VALUE, VALUE]),
"putd" => InstructionSignature(&[VALUE, VALUE, VALUE]),
"bdnvl" => InstructionSignature(&[DEVICE, LOGIC_TYPE, VALUE]),
"bdnvs" => InstructionSignature(&[DEVICE, LOGIC_TYPE, VALUE]),
};

pub(crate) const LOGIC_TYPES: phf::Set<&'static str> = phf_set! {
Expand Down Expand Up @@ -431,6 +457,7 @@ impl<'a> From<&'a [DataType]> for Union<'a> {
}

impl<'a> Union<'a> {
/// True if `typ` is one of this union's alternatives.
pub(crate) fn match_type(&self, typ: DataType) -> bool {
for x in self.0 {
if *x == typ {
Expand All @@ -440,6 +467,8 @@ impl<'a> Union<'a> {
false
}

/// True if the two unions share at least one alternative. Used to check
/// whether a found operand type satisfies a parameter's expected type.
pub(crate) fn match_union(&self, types: &Union) -> bool {
for typ in self.0 {
for typ2 in types.0 {
Expand All @@ -451,6 +480,7 @@ impl<'a> Union<'a> {
false
}

/// This union's alternatives that also appear in `other`.
pub(crate) fn intersection(&self, other: &[DataType]) -> Vec<DataType> {
self.0
.iter()
Expand All @@ -460,6 +490,11 @@ impl<'a> Union<'a> {
}
}

/// Which `DataType`s a bare identifier could be read as if used where a
/// `logictype` operand is expected. Several of the name sets overlap (e.g.
/// `"Maximum"` is both a `LogicType` and a `BatchMode`), so this can return
/// more than one candidate; callers narrow it further using the operand's
/// expected `Union` from the instruction's signature.
pub(crate) fn logictype_candidates(text: &str) -> Vec<DataType> {
let mut ret = Vec::with_capacity(3);

Expand Down Expand Up @@ -591,6 +626,10 @@ pub(crate) const INSTRUCTION_DOCS: phf::Map<&'static str, &'static str> = phf_ma
"peek" => "Register = the value at the top of the stack",
"push" => "Pushes the value of a to the stack at sp and increments sp",
"pop" => "Register = the value at the top of the stack and decrements sp",
"get" => "Loads the value in the stack memory at index address on provided device into register r?.",
"getd" => "Loads the value in the stack memory at index address on provided device id into register r?.",
"put" => "Adds the value to the stack memory off the provided device at index address.",
"putd" => "Adds the value to the stack memory off the provided device id at index address",
"hcf" => "Halt and catch fire",
"select" => "Register = b if a is non-zero, otherwise c",
"sleep" => "Pauses execution on the IC for a seconds",
Expand All @@ -601,6 +640,23 @@ pub(crate) const INSTRUCTION_DOCS: phf::Map<&'static str, &'static str> = phf_ma
"acos" => "Returns the angle (radians) whos cosine is the specified value",
"atan" => "Returns the angle (radians) whos tan is the specified value",
"atan2" => "Returns the angle (radians) whose tangent is the quotient of two specified values: a (y) and b (x)",
"brnan" => "Relative branch to line b if a is not a number (NaN)",
"lbns" => "Loads LogicSlotType from slotIndex from all output network devices with provided type and name hashes using the provide batch mode. Average (0), Sum (1), Minimum (2), Maximum (3). Can use either the word, or the number.",
"sbs" => "Stores register value to LogicSlotType on all output network devices with provided type hash in the provided slot.",
"sra" => "Performs a bitwise arithmetic right shift operation on the binary representation of a value. It shifts the bits to the right and fills the vacated leftmost bits with a copy of the sign bit (the most significant bit).",
"sla" => "Performs a bitwise arithmetic left shift operation on the binary representation of a value. It shifts the bits to the left and fills the vacated rightmost bits with zeros (note that this is indistinguishable from 'sll').",
"sll" => "Performs a bitwise logical left shift operation on the binary representation of a value. It shifts the bits to the left and fills the vacated rightmost bits with zeros.",
"bnan" => "Branch to line b if a is not a number (NaN)",
"lbs" => "Loads LogicSlotType from slotIndex from all output network devices with provided type hash using the provide batch mode. Average (0), Sum (1), Minimum (2), Maximum (3). Can use either the word, or the number.",
"not" => "Performs a bitwise logical NOT operation flipping each bit of the input value, resulting in a binary complement. If a bit is 1, it becomes 0, and if a bit is 0, it becomes 1.",
"snanz" => "Register = 0 if a is NaN, otherwise 1",
"ss" => "Stores register value to device stored in a slot LogicSlotType on device.",
"sbn" => "Stores register value to LogicType on all output network devices with provided type hash and name.",
"srl" => "Performs a bitwise logical right shift operation on the binary representation of a value. It shifts the bits to the right and fills the vacated leftmost bits with zeros",
"snan" => "Register = 1 if a is NaN, otherwise 0",
"lbn" => "Loads LogicType from all output network devices with provided type and name hashes using the provide batch mode. Average (0), Sum (1), Minimum (2), Maximum (3). Can use either the word, or the number.",
"bdnvl" => "Branch to line c if the provided logic type is not \'loadable\', that is the device would provide an exception if you tried to use LOAD(\"l\")",
"bdnvs" => "Branch to line c if the provided logic type is not \'storable\', that is the device would provide an exception if you tried to use STORE(\"s\")",
};

pub(crate) const LOGIC_TYPE_DOCS: phf::Map<&'static str, &'static str> = phf_map! {
Expand Down
Loading