Editor Support: the Language Server​

jaiscript_lsp is the JaiScript language server. It speaks the Language Server Protocol over stdin/stdout and answers from the engine's own lexer, parser and strict checker, so what the editor underlines is what the engine reports when the program loads. The VS Code extension in tools/vscode-jaiscript/ launches it for every .jai file; any other LSP client can run it the same way.

One checker, two places. The server does not reimplement the language. It parses with the engine's parser, checks with the checker that gates execute() (Static Checking), and builds navigation from the same parse and the checker's inferred types. A clean program shows no diagnostics in the editor for the same reason it loads: the checker's false-positive contract.

Quick start​

  1. Build the server from the JaiScript folder: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release, then cmake --build build --target jaiscript_lsp. The executable is build/bin/jaiscript_lsp (.exe on Windows).
  2. Open tools/vscode-jaiscript in VS Code and press F5, or link or install the extension (below). Run from tools/vscode-jaiscript, the extension finds the server step 1 built without any setting; otherwise set jaiscript.languageServer.path to the executable, or put its folder on PATH.
  3. Open a .jai file. Diagnostics appear as you type; F12 goes to a definition, Shift+F12 lists references, F2 renames.
  4. Scripts written for your own host: declare its names with extern, or build a server with your registrations (The host surface).

If the server is not found the extension says so (jaiscript_lsp was not found); point jaiscript.languageServer.path at the executable.

What it does​

  • Diagnostics as you type. Parse errors and every checker finding, errors and warnings, with the checker's own text. The edited file is re-checked on each change; the other files of its program are re-checked in the background when a change alters what they can see (a declaration, a signature, a field). An error in an imported file is reported in that file, at its line. A file that does not parse reports its parse error, and the names it declares stay declared for the files that import it: one syntax error is one diagnostic, not a list of undeclared identifiers elsewhere.
  • Go to definition and find references for functions, classes, fields, methods, constructors, globals, locals, parameters, enums and their values, and namespace members, across imports and includes. Definition on the path of an import or include opens that file.
  • Hover: a variable's declared type, or for auto the type the checker inferred; a function's or method's signatures; a class with its fields and methods; an enum value with its number; a host function with the parameter types it was registered with.
  • Completion: locals and parameters in scope, the enclosing class's members, the program's globals, functions, classes, enums and namespaces, the host's functions and classes, and keywords; after ., the receiver's members (inherited ones included, registered C++ classes included) or the builtin methods of an array, map or string; after ::, a namespace's members, a class's statics or an enum's values.
  • Signature help inside a call's parentheses, every overload, the current parameter highlighted.
  • Rename across every file of the program. A rename that cannot be proven complete is refused with the reason instead of applied halfway (see Rename).
  • Document symbols (the outline: classes with their members, functions, globals, enums, namespaces) and workspace symbols (every declaration in the workspace's .jai files, fuzzy-matched).

Programs: what is checked together​

A file whose top-level import and include declarations carry literal paths is the root of a program, and the checker checks a program as one: a function in one file may use a global or a class another file declares, in any import order (Across files). The server gives each file the view the engine gives it when its program runs. It reads the workspace's import graph, walks up from the file to the files nothing imports (the program's roots), and checks the file against the names and class declarations of every unit in those roots' closures. A file shared by two programs sees both. A file nothing imports is its own root.

Paths resolve with the engine's one ladder: the importing file's folder, then the configured include paths, then the working directory, which the server sets to the first workspace folder. The file an editor has open is read from the editor, unsaved changes included; every other file is read from disk and re-read when it changes.

The host surface​

A script can call what its host registers, and the checker knows those names only from the engine it checks against. The server builds that engine from a host profile:

hostregistrations
runner (default)the jaiscript runner's exact surface: the standard library, file IO, the console and audio functions, NetServer/NetChannel, Download, ARGS. The server compiles the runner's own registration code (tools/lsp/runner_host.cpp), so the two never drift.
stdlibjai::stdlib::register_all alone.
barea fresh engine: the language and the engine-core builtins every engine has (print, format_value, math::, binary::, extend, same_object, parallel_transform, the file-location functions), without the standard library's to_string, type_of, to_json, time:: or reflect:: (whose is_enabled alone is core).

Scripts written for another host (a game engine with its own bound classes) have two routes. Names the host provides can be declared in the scripts with extern, which the checker, and therefore the server, reads as declarations (Static Checking). Or the host builds its own server: the server is six source files over the library, and runner_host.cpp is the one that registers the surface; a host replaces it with a file that calls its own registration function.

Building the server​

The server is the jaiscript_lsp CMake target (JAISCRIPT_BUILD_LSP, on by default when JaiScript is the top-level CMake project and off as a subproject). It has no dependency beyond the library and what the runner links.

shell
cmake --build build --target jaiscript_lsp       # -> build/bin/jaiscript_lsp(.exe)

jaiscript_lsp --host=runner|stdlib|bare   # the host profile (default runner)
              --include=<dir>             # an include path (repeatable)
              --max-message=<bytes>       # the largest message body read (default 64 MiB, at least 65536; a larger one is skipped)
              --log                       # each request and its time, on stderr

Installing the VS Code extension​

The extension folder is ready to run as it is in the repository: out/extension.js is a single bundled file, the language client included. Install it in one of three ways:

shell
# 1. try it: an Extension Development Host window
code tools/vscode-jaiscript          # then press F5 and open a .jai file there

# 2. install it from the repository (a link keeps it current)
#    Windows:  mklink /J "%USERPROFILE%\.vscode\extensions\snapjaw.jaiscript" tools\vscode-jaiscript
#    else:     ln -s "$PWD/tools/vscode-jaiscript" ~/.vscode/extensions/snapjaw.jaiscript

# 3. package it
cd tools/vscode-jaiscript
npm install
npx @vscode/vsce package             # jaiscript-0.5.0.vsix
code --install-extension jaiscript-0.5.0.vsix

The extension finds the server in this order: jaiscript.languageServer.path when it is set; server/jaiscript_lsp (.exe on Windows) inside the extension folder, where a packaged build carries it; the JaiScript folder's build trees, two levels above the extension folder (so found when the extension runs from tools/vscode-jaiscript): build/bin, then build-debug/bin, then the trees Visual Studio's CMake presets create there (Release before Debug); then PATH. Rebuilding the extension itself is npm install and npm run compile (a type check, then the bundle).

settingdefaultmeaning
jaiscript.languageServer.enabledtruerun the server for .jai files
jaiscript.languageServer.pathemptythe server executable; ${workspaceFolder} expands
jaiscript.languageServer.hostrunnerthe host profile (above)
jaiscript.languageServer.includePaths[]include paths for import and include
jaiscript.languageServer.logfalselog requests and timings to the JaiScript Language Server output channel

A settings change restarts the server; JaiScript: Restart Language Server in the command palette does it by hand. The debugger attach (Debugging) is the same extension and works with or without the server.

Rename​

A rename edits the declaration and every use in every file of the declaring file's programs. Renaming a method renames it in every class of its hierarchy that declares the name, so an override keeps overriding. A rename is refused, with the reason, when the server cannot prove the edit complete or correct:

  • a member whose name is also used on a receiver of unknown type (a var, a dynamic value): that use might be this member, and the server does not guess;
  • a global declared in more than one file: each file sees its own declaration (Modules), so one rename would change which one the others see;
  • a new name already declared where the old one is visible (the class already has that member, the program already has that global, the scope already has that local);
  • a name the host or the language defines.

Performance​

A change re-parses the edited file and re-checks it against its program's tables. The other files' parses stay cached, the program's tables are rebuilt from cached per-file declarations only when a file's declarations change, and only such a change sends the rest of the program to be re-checked, one file at a time between requests. Navigation reads a per-file model built from the same check and kept until the file or its program changes.

Measured on Gator Quest (examples/gatorquest: 36 files, 13,980 lines; a Release build on a desktop Windows machine; times from the request sent to the answer received, over stdio):

operationtime
an edit inside a function of gq_combat.jai (464 lines), to its diagnostics3 ms median, 4 ms at the 95th percentile
the same in gq_actions.jai (1,085 lines, the largest file)10 ms median, 11 ms at the 95th percentile
opening the root: every file parsed and the whole program checkedabout 210 ms
an edit that changes a declaration: the rest of the program re-checkedabout 60 ms
find references across the program, first time (every file's model built) / after130 ms / 4 ms
completion after CHARS[0].20 ms

The measurement is part of the server's protocol tests: python tools/lsp/tests/lsp_test.py --only latency.

Other editors​

Any LSP client runs the server as a stdio process for the jaiscript language and .jai files. The initialization options mirror the extension's settings:

initializationOptions
{ "host": "runner", "includePaths": ["scripts/shared"] }

The server declares its capabilities in the usual way (textDocumentSync incremental, hover, definition, references, document highlights, completion on ., :: and ->, signature help on ( and ,, rename with prepare, document and workspace symbols) and reads file changes from workspace/didChangeWatchedFiles for **/*.jai.

What it does not see​

  • Dynamic values. A member used on a var has no definition to go to and nothing to hover, and a var variable hovers as var: the checker's precision map (Static Checking) is the server's too. Declared types, auto and references to declared types navigate.
  • Run-time structure. Scripts are never executed. Members a script adds with extend, names a host registers after load, and files reached by a computed import(expr) are outside what it knows; extern declares the names.
  • A declaration that does not parse. While a declaration has a syntax error, the parser's error is what the editor shows for it; checker findings stay for the code before the error and for whole declarations after it, and the file's other declarations keep their navigation. Inside the broken declaration, completion and signature help work from the text before the cursor.