How did we get here?
If you did not expect the author to be some random guy that had rarely even been using Macaulay before, then don’t worry as so did I. After I’ve opened iot once to make some calculations, I’ve noticed that it was probably the last alive language on Earth with no language server implemented yet. I thought it must be a fun hobby project to work on next month. 11 months later the alpha build is here, together with every single shower thought of anything that could potentially be build from that.
After all I ended up with way more of them than I’ve expected, so that releasing them one by one would be a bit of a pain, so I decided to bundle them all together into one bundle where I could manage tyhem all together and youy could browse them all together.
Tools included
- Language server: static code analyzer including, type-checker, formatter, linter and fully featured language server. It’s written in Rust and fully independent on M2, so you can use it even on Windows.
- spectral-m2: A new shell preserving the good old experience with some new refreshed TUI interface and new renderer supporting rich highlighting and customisable styling.
- tree-sitter: Fast, incremental parser with rich syntax highlighting
- MarkdownDocs: Generating documentation as markdowns and mdbook-based web builder, giving documentation a fresh look.
- macaulay2.fun: A web service hosting the docs build with
mdbook - VSCode extension: Porting LSP and tree-sitter to work in VSCode
- mdbook-tsitter: Preprocessor for
mdbookthat uses tree-sitter, making it properly highlight m2 code blocks. - Neovim plugin: Mostly for ease configuration of LSP and tree-sitter, with a few extra features like inline cell evaluation.
- m2-syn Rust crate with native parser generating fully typed AST, syntax tree traversal, AST renderer
Tools in late WIP stage
- ProceduralMacros Macaulay2 native procedural macro engine
- m2-syn Macro engine and fully incremental parsing
- Macaulay2 Book A restructured documentation making it more casually readable rather than serving as fully generated reference
- New documentation API See the Proposal chapter
What is still lacking
Proper emacs support and integration. As I’ve never used emacs, the bundle was developed possibly in parallel with the popular emacs developer environment. This doesn’t mean that emacs compatibility is an actual issue, but rather I just don’t feel competent to provide a proper support for it, and that I may have duplicate some systems that already exist there for ages.
Who it’s for
Every m2 user is likely to find something that will make his life easier - trying out the language server is probably a good starting point.
Quickstart
Language server
| Requires | cargo |
| Platforms | linux, macOS, Windows |
| Successfully tested on | Arch |
| Documentation | spectralflux.dev/decs/m2-ls.html |
| Repository | github.com/AlexanderGolys/m2-ls.git |
| crates.io | crates.io/crates/m2-ls |
The m2-ls language server is cross platform binary Rust crate with no additional dependencies needed to run the binary.
To install it from cargo toolchain run
cargo install m2-ls
To run it from source code, clone the repository and run
cargo run --bin m2-ls
To configure the client in code editors, see LSP setup
Linting and formatting
Linter and formatter used in
m2-lsare also available as standalone binaries. After installing the crate, you can run them in terminal usingm2-fmt [file] m2-lint [file]
VS Code extension
Search for the Macaulay2 LSP extension and you’re done.
Documentation · Marketplace · Repository
m2.nvim
| Requires | Neovim 0.11+, M2 for running code |
| Platforms | linux, macOS, Windows |
| Successfully tested on | Arch |
| Documentation | Nvim plugin |
| Repository | github.com/AlexanderGolys/m2.nvim |
With lazy.nvim add
{
"AlexanderGolys/m2.nvim",
dependencies = { "nvim-treesitter/nvim-treesitter" },
opts = {},
}
The plugin doesn’t start the language server by itself, see LSP in Nvim for the few lines that do.
Running code
<leader>moopens M2 in a split,<leader>mcsends the expression under the cursor (or the selection) to it.
spectral-m2
| Requires | cargo, M2 |
| Platforms | linux, macOS |
| Successfully tested on | Arch |
| Documentation | spectral-m2 |
| Repository | github.com/AlexanderGolys/spectral-m2 |
| crates.io | crates.io/crates/spectral-m2 |
spectral-m2 runs your installed M2 underneath, so M2 needs to be on the PATH.
To install it run
cargo install spectral-m2 --locked
and start it with spectral-m2 (or just sp2) instead of M2. Typing / on an empty line lists all commands.
Fonts
Icons need a Nerd Font. Without one, set
SPECTRAL_M2_BASIC_GLYPHS=1.
MarkdownDocs
| Requires | M2, mdbook |
| Platforms | linux, macOS |
| Successfully tested on | Arch |
| Documentation | MarkdownDocs |
| Repository | github.com/AlexanderGolys/MarkdownDocs |
Clone the repository and install the package from M2
installPackage("MarkdownDocs", FileName => "/path/to/MarkdownDocs.m2")
mdbook-tsitter
| Requires | cargo, mdbook 0.5, git, C compiler |
| Platforms | linux, macOS, Windows |
| Successfully tested on | Arch |
| Documentation | spectralflux.dev/mdbook-tsitter |
| Repository | github.com/AlexanderGolys/mdbook-tsitter |
| crates.io | crates.io/crates/mdbook-tsitter |
To install it run
cargo install mdbook-tsitter
and enable it in book.toml
[preprocessor.tsitter]
First build
Grammars are compiled on first use, which is why
gitand a C compiler are needed. After that they’re cached.
ProceduralMacros
| Requires | M2 1.26.05+ |
| Platforms | linux, macOS |
| Successfully tested on | Arch |
| Documentation | ProceduralMacros |
| Repository | github.com/AlexanderGolys/ProceduralMacros |
Clone the repository and install the package from M2
installPackage("ProceduralMacros", FileName => "/path/to/ProceduralMacros.m2")
m2-syn
| Requires | cargo |
| Platforms | linux, macOS, Windows |
| Successfully tested on | Arch |
| Documentation | docs.rs/m2-syn |
| Repository | github.com/AlexanderGolys/m2-syn |
| crates.io | crates.io/crates/m2-syn |
m2-syn is a library crate. To add it to your Rust project run
cargo add m2-syn
Inspecting a file
To see the syntax tree of a file, clone the repository and run
cargo run --example inspect -- file.m2
tree-sitter-macaulay2
| Requires | cargo, npm or pip |
| Platforms | linux, macOS, Windows |
| Successfully tested on | Arch |
| Documentation | tree-sitter-macaulay2 |
| Repository | github.com/AlexanderGolys/tree-sitter-macaulay2 |
| crates.io | crates.io/crates/tree-sitter-macaulay2 |
| npm | npmjs.com/package/tree-sitter-macaulay2 |
| PyPI | pypi.org/project/tree-sitter-macaulay2 |
The grammar is published for Rust, Node and Python
cargo add tree-sitter tree-sitter-macaulay2
npm install tree-sitter-macaulay2
pip install tree-sitter-macaulay2
Editors
To use the grammar for highlighting in an editor, see Setting up tree-sitter.
See also
List of project specific documentations
- spectral-m2: spectralflux.dev/docs/m2-ls.html
- m2-ls: spectralflux.dev/docs/m2-ls.html
- m2-syn: docs.rs/m2-syn
- mdbook-tsitter: spectralflux.dev/docs/m2-ls.html
- MarkdownDocs: spectralflux.dev/docs/m2-ls.html
mdbook builds of Macaulay2 docs
My other projects
Contact
- email: alexandergolys@gmail.com
- X: @olekgolys
spectral-m2
A new face for the M2 prompt. It’s still the good old REPL underneath, just with a TUI that doesn’t feel like 1995.
cargo install spectral-m2 --locked
sp2
What’s nicer
- Highlighting as you type, and Enter on an unfinished expression just continues it on the next line.
- Rendered output: TeX becomes actual maths in the terminal (stacked fractions,
ℚif you like), long outputs fold, andhelppages show up inline with clickable links. - Side views of your session: just the code, just the values, printed output, or the syntax tree of what you typed.
- History with prefix search: type
ide, press Up, get only things starting withide. Shared live between open shells. - Sessions: your code is saved on exit and
/resumebrings it back (by re-running it)./saveand/loadwork with any.m2file. - Colour schemes: catppuccin, tokyo night, gruvbox, everforest, rosé pine, kanagawa… light and dark.
Your init.m2 and installed packages load as usual.
Getting around
Type / on an empty line and everything is listed. The ones you’ll use most:
| Alt+C / V / P / T | View code / values / printed output / syntax tree |
| Shift+Enter | New line |
| Alt+R | Restart M2 |
| Ctrl+O | Settings (theme lives here) |
| Alt+K | Rebind keys |
| Ctrl+C | Quit |
You’ll need
M2on yourPATH- A terminal with true colour. Kitty, WezTerm, Ghostty, foot and the like are perfect.
- A Nerd Font for the icons, or
SPECTRAL_M2_BASIC_GLYPHS=1
Not yet
You can’t interrupt a running computation yet (a cell gives up after 5 minutes), and it’s one evaluation at a time.
m2-ls
The language server: the thing that started all of this. It gives any editor with an LSP client errors as you type, hover docs, go-to-definition, rename, completion, formatting and a type checker.
It’s written in Rust and never runs Macaulay2. Everything is static, and M2’s documentation is baked into the binary, so it starts instantly, works without M2 installed (yes, even on Windows), and can’t get stuck on a Gröbner basis.
What it does
- Diagnostics: syntax errors, code M2 rejects or quietly ignores, type errors, style nits. More on the linter page.
- Type checker: infers types without running anything, from the return types M2 declares for its own methods. If it isn’t sure, it keeps quiet: a type error it shows should be real.
- Hover: docs for builtins and for your own code, plus the inferred type (how it works).
- Navigation: definition, references, implementations, type hierarchy, symbols, and jumping into M2’s library sources if you have M2 installed.
- Rename across files, inlay type hints, signature help, semantic highlighting.
- Quick fixes and refactors, e.g.
L.1→L#1or “flatten nested if”. - Formatting: see formatter.
It follows needsPackage, load, needs and friends, knows what a package exports, and
checks TEST blocks like normal code.
n = 5
if n then print "yes" -- warning: if condition must have type `Boolean`, but this has type `ZZ`
Install
cargo install m2-ls
You get m2-ls, m2-lint and m2-fmt. Then
hook up your editor. In VS Code skip all that, since the
extension brings the server along on Linux, macOS and Windows.
Settings
Under the m2-ls section of your editor’s LSP config:
{
"m2-ls": {
"diagnostics": { "disabled": ["unused-binding", "T02"] },
"formatting": { "indent": 4 },
"inlayHints": { "expressionTypes": true },
"sources": { "paths": ["~/opt/macaulay2"] }
}
}
The full list is in the repository README.
Known gaps
- Things that only exist at runtime (
value "x", symbols made on the fly) stay unknown. - Completion is sparse on purpose: it shows up when there are a few good candidates, not 400.
- Only files reachable from what you open are indexed, not the whole project folder.
Under the hood
Parsing is m2-syn on top of the tree-sitter grammar, incremental as you type. Builtin docs and signatures come from an index extracted from Macaulay2’s own documentation.
m2-ls is MIT. The only exception is that embedded documentation, which stays under Macaulay2’s GPL.
Formatter
Part of m2-ls, and also a standalone m2-fmt.
It fixes spacing, derives indentation from the syntax tree and wraps long lines where the syntax allows. It doesn’t change what your code does: dropping redundant parentheses and such is left to quick fixes.
f=x->(if x>0 then 1 else 2) -- before
f = x -> (if x > 0 then 1 else 2) -- after
In the editor, just use “format document”. On the command line:
m2-fmt file.m2 # in place
m2-fmt --check file.m2 # exit 1 if not formatted, handy in CI
m2-fmt --stdout file.m2 # print instead of writing
Files with syntax errors are left alone.
Knobs
Under m2-ls.formatting (or --config settings.json for m2-fmt):
indent,useTabs: default to your editor’s settings, 4 spaces on the CLIsoftLineWidth/hardLineWidth: preferred and maximum line width, 78 / 90controlFlowLayout:compact,multilineormultilineCompactElse(default)compactFactorOperators:2*xinstead of2 * xbreakAfterSemicolon: new line after every;
Linter
Also part of m2-ls, and on the command line as m2-lint.
Rules come in four groups:
- X, syntax: parse errors, and code that parses but means something else (
L.1is notL#1) - E, M2 semantics: things M2 rejects or silently ignores, like installing a method on a
plain function,
=where:=is needed, orreturnoutside a function - T, types: an
ifon something that can’t beBoolean, a declared return type that contradicts the code - S, style: unused locals, lowercase option keys,
if (c) then, null checks that could be??
A lot of them come with a quick fix. Unused locals starting with _ are fine.
Each rule has a code and a name (S02 = unused-binding); the full list is in the
m2-ls README. Turn rules off or down in settings:
{ "m2-ls": { "diagnostics": { "disabled": ["S06"], "warningsAsHint": ["unused-binding"] } } }
In CI
m2-lint file.m2
m2-lint --deny-warnings --config settings.json file.m2
t.m2:2:4: warning[T02]: if condition must have type `Boolean`, but this expression has type `ZZ`
Exit code 1 on errors (or warnings, with --deny-warnings).
MarkdownDocs
An M2 package that turns documentation into Markdown. It works on the installed docs of any
package, so the output is exactly what help shows, just in a format that can go anywhere:
an mdBook site, a GitHub wiki, an Obsidian vault.
needsPackage "MarkdownDocs"
mdBook "Graphs" -- build the Graphs docs as a book and open it in the browser
Markdown files
writeMdDocs("Graphs", "docs")
writes docs/Graphs/ with an index and one page per documentation node.
LinkedPackages => true also writes the packages it links to, and
LinkStyle => "wikilink" gives [[links]] for Obsidian. Examples come with the output stored
at install time; nothing is re-run.
As a library
render markdown help "ideal"
markdown turns any M2 hypertext into a small Markdown tree, render makes it text, and in
between you can reshuffle it however you like.
Install
installPackage("MarkdownDocs", FileName => "/path/to/MarkdownDocs/MarkdownDocs.m2")
Websites are on the next page: mdBook-based builder.
mdBook-based builder
MarkdownDocs can also lay the docs out as an mdBook: search, dark mode, a sidebar that makes sense, M2 highlighted by mdbook-tsitter, maths with KaTeX.
mdBook "Graphs"
builds it, serves it locally and opens the browser, like viewHelp. The server goes away
when M2 does.
The book layout
The docs are grouped the way you’d read them, not the way they’re stored: all methods of a
function are chapters of that function’s page, and a package’s methods on someone else’s
function (say degree of a graph) get a page in that package. mdDocs gives the plain
one-page-per-node layout instead.
Your package as a website
writeMdBook("MyPackage", "docs", LinkedPackages => true)
writeBookFiles "docs/MyPackage"
mdbook build docs/MyPackage -d "$PWD/site/MyPackage"
Build every package’s book side by side so the links between them work, and site/ can go
straight to GitHub Pages.
Needs mdBook; mdbook-tsitter and mdbook-katex are optional but recommended.
Refreshed web service
All of the Macaulay2 docs, rebuilt with the tools from this bundle and put online.
- search across every package at once
- dark mode, and a sidebar you won’t get lost in
- M2 code actually highlighted
- the book layout, with methods grouped under their function
Three tabs at the top:
- Reference: every object in M2, generated from M2 itself
- Original: M2’s own docs, one book per package
- Guide: the Macaulay2 Book
The docs are extracted from a running M2, converted to Markdown and built with mdBook; the same extracted data feeds the language server’s hover. Want the same for your package? See the mdBook-based builder.
The content itself belongs to the Macaulay2 and package authors.
Macaulay2 Book
Work in progress. Not much to read yet.
The reference documentation is great once you know what you’re looking for, but it’s a reference: generated page by page, one per function. The Macaulay2 Book is meant to be read front to back: a guide to the language and its ideas, the way the Rust Book is for Rust.
It’s the “Guide” tab of the web service.
Proposal: towards new API
This is a proposal, not something you can install. Opinions very welcome.
Writing M2 docs today means a separate doc ///…/// block (or a document {…} list) far away
from the code, in a hypertext DSL of its own. It works, but it’s a chore, so a lot of code ends
up documented badly or not at all.
While building the language server I ended up needing docs in a different shape anyway: as data, attached to the code, and in Markdown. The next pages are about what that looks like and where I think it could go:
- How LSP builds its hover: where the docs you see in the editor come from
- Inline docs and annotation: documenting code right where it’s written
How LSP builds its hover
The server never runs M2, so hover docs have to come from somewhere else.
Builtins come from an index extracted once from Macaulay2’s own documentation and compiled into the server. Every function, type and method has a Markdown page there, plus its signatures and return types (which also feed the type checker). On hover, the signature is narrowed down to the method that’s actually being called.
Your code gets:
- the inferred type and what the thing is (local, global, method, option, …)
- the methods you installed on it
- your doc comment, if there is one; see Inline docs
So a function you wrote two minutes ago already has a useful hover, and adding one comment line above it makes it as good as a builtin’s.
Inline docs and annotation
The language server picks up plain comments as documentation. No new syntax, just comments in the right spot, written in Markdown.
Above an assignment: consecutive -- lines right before it, no blank line between.
-- Number of edges of `G` between the vertices in `S`.
-- See also [[inducedSubgraph]].
edgesWithin = (G, S) -> #edges inducedSubgraph(G, S)
First thing in a function body: a -* … *- block.
edgesWithin = (G, S) -> (
-* Number of edges of `G` between the vertices in `S`. *-
#edges inducedSubgraph(G, S)
)
The whole package: a -* … *- block at the top of the file, before newPackage.
[[name]] links to another symbol: it’s clickable in the editor, and it gets updated when you
rename that symbol. Code spans and fenced blocks are highlighted as M2.
Annotations that the type checker reads are the return types you can already declare in
M2, f Type := ReturnType => x -> …. If it can work one out, the server offers to add it for you.
Inline docs and annotation
The language server picks up plain comments as documentation. No new syntax, just comments in the right spot, written in Markdown.
Above an assignment: consecutive -- lines right before it, no blank line between.
-- Number of edges of `G` between the vertices in `S`.
-- See also [[inducedSubgraph]].
edgesWithin = (G, S) -> #edges inducedSubgraph(G, S)
First thing in a function body: a -* … *- block.
edgesWithin = (G, S) -> (
-* Number of edges of `G` between the vertices in `S`. *-
#edges inducedSubgraph(G, S)
)
The whole package: a -* … *- block at the top of the file, before newPackage.
[[name]] links to another symbol: it’s clickable in the editor, and it gets updated when you
rename that symbol. Code spans and fenced blocks are highlighted as M2.
Annotations that the type checker reads are the return types you can already declare in
M2, f Type := ReturnType => x -> …. If it can work one out, the server offers to add it for you.
mdbook-tsitter
mdBook highlights code with highlight.js, which has never heard of Macaulay2, so M2 in a book was just grey text. This preprocessor highlights code blocks with real tree-sitter grammars at build time instead. This book uses it.
Not M2-only: it knows 300+ languages, and they look better too, because they’re parsed rather than regex-guessed.
cargo install mdbook-tsitter
# book.toml
[preprocessor.tsitter]
That’s the whole setup; fences tagged m2 (or anything else) are highlighted. Grammars are
compiled on first use, so it needs git and a C compiler once.
Extras: notreesitter on a fence falls back to highlight.js, pane="Title" puts blocks side
by side, and mdbook-tsitter css > theme/highlight_ts.css gives you the stylesheet to tweak.
It follows mdBook’s light and dark themes.
Repository · Docs · crates.io
VSCode extension
The easy way in: Macaulay2 LSP comes with the language server inside, so installing it is the whole setup. Works on Linux, macOS and Windows, and you don’t need Rust or even Macaulay2 installed.
Search for Macaulay2 LSP in the Extensions view, or:
code --install-extension AlexanderGolys.macaulay2-language-server
Open a .m2 file and you’re done.
What you get
All the usual VS Code moves now work on M2:
| Hover | docs of builtins and your own functions, plus the inferred type |
| F12 / Shift+F12 | go to definition / find references |
| F2 | rename, across files |
| Ctrl+. | quick fixes and refactors on the line |
| Ctrl+Space | completion: package names, option keys, symbols |
| Ctrl+Shift+O / Ctrl+T | symbols in the file / in everything loaded |
| Format Document | the formatter (Shift+Alt+F, Ctrl+Shift+I on Linux) |
| Problems panel | everything the linter and type checker found |
Plus folding, highlighting of the symbol under the cursor, comment toggling with -- and
-* … *-, and auto-closing brackets (including <| |>).
Highlighting comes from the server, not a regex grammar, so it knows what a name is: a function, a type, a local, an option. Colours show up a moment after the file opens.
Type hints are there but hidden, so they don’t clutter the code: hold Ctrl+Alt to see them.
Running code from the editor isn’t there yet; that’s Neovim-only for now.
Settings
Everything is under Macaulay2 in the Settings UI, or macaulay2.* in settings.json.
Changes apply right away, except server.*, which needs a window reload.
Diagnostics
| Setting | Default | |
|---|---|---|
macaulay2.diagnostics.enabled | true | all diagnostics on/off |
macaulay2.diagnostics.disabled | [] | rules to hide, by name or code |
The Settings UI offers every rule with a short explanation, so you don’t need to remember
that S06 is ring-variable-naming.
Formatting
| Setting | Default | |
|---|---|---|
macaulay2.formatting.indentWidth | editor’s | spaces per level |
macaulay2.formatting.useTabs | editor’s | indent with tabs |
macaulay2.formatting.softLineWidth | 100 | width it prefers to wrap at, 0 = off |
macaulay2.formatting.hardLineWidth | 100 | width it always wraps at, 0 = off |
macaulay2.formatting.controlFlowLayout | multilineCompactElse | compact, multiline or multilineCompactElse |
macaulay2.formatting.compactFactorOperators | false | 2*x instead of 2 * x |
macaulay2.formatting.breakAfterSemicolon | true | new line after ; |
Inlay hints
| Setting | Default | |
|---|---|---|
macaulay2.inlayHints.expressionTypes | false | also types of arguments, subexpressions and call results |
macaulay2.inlayHints.allKnownTypes | false | even the obvious ones, like the type of a number literal |
Server
| Setting | Default | |
|---|---|---|
macaulay2.server.path | bundled | your own m2-ls build |
macaulay2.server.arguments | [] | extra arguments for it |
macaulay2.trace.server | off | log LSP traffic (messages, verbose) in the Output panel |
A nice setup
{
"[macaulay2]": {
"editor.formatOnSave": true,
"editor.inlayHints.enabled": "on" // always show types
},
"macaulay2.diagnostics.disabled": ["ring-variable-naming"],
"macaulay2.formatting.hardLineWidth": 90
}
Something’s off?
Set macaulay2.trace.server to messages, reproduce it, and grab the log from
Output → Macaulay2 Language Server for the bug report.
Nvim plugin
m2.nvim makes Neovim feel at home with .m2 files and lets you run code from the buffer.
-- lazy.nvim
{
"AlexanderGolys/m2.nvim",
dependencies = { "nvim-treesitter/nvim-treesitter" },
opts = {},
}
Neovim 0.11+.
What you get
- filetype detection, comments, indentation,
:makerunning the file through M2 with errors in quickfix - folding and indentation from the tree-sitter grammar
- an M2 terminal next to your code, and keys to send code to it
The language server isn’t started by the plugin; it’s a few lines, see here.
Running code
| Keys | |
|---|---|
<leader>mo | open M2 in a split |
<leader>mc | send the cell under the cursor (or the selection) |
<leader>ml | send the current line |
<leader>mt | hide / show the terminal (M2 keeps running) |
<leader>mq | close it |
A cell is just the top-level expression under the cursor, so no -- %% markers. A
multi-line call goes as one piece.
Keys can be changed with opts.term.keys, or switched off with term = false.
Setting up tree-sitter
The tree-sitter grammar gives editors highlighting, folding and indentation that understand M2’s actual syntax.
You may not need this at all:
- VS Code: the extension gets highlighting from the language server, nothing to set up.
- Any editor running m2-ls: semantic highlighting from the server already covers most of what tree-sitter would. Tree-sitter adds instant highlighting before the server starts, folding and indentation.
Setting up tree-sitter in Emacs
Not available yet. Emacs 29+ can load the grammar itself, but highlighting also needs a
macaulay2-ts-modethat maps the grammar’s captures to faces, and nobody has written one.
Until then, Macaulay2’s own M2-mode handles highlighting, and the
language server adds semantic highlighting on top.
If you’d like to write the major mode, the grammar’s queries/highlights.scm is the place to
start; contributions are very welcome.
Setting up tree-sitter in Nvim
The grammar isn’t shipped with nvim-treesitter, so it has to be registered by hand. With the
nvim-treesitter master branch:
vim.filetype.add({ extension = { m2 = "macaulay2" } }) -- not needed with m2.nvim
require("nvim-treesitter.parsers").get_parser_configs().macaulay2 = {
install_info = {
url = "https://github.com/AlexanderGolys/tree-sitter-macaulay2",
files = { "src/parser.c", "src/scanner.c" },
},
filetype = "macaulay2",
}
Then install the parser:
:TSInstall macaulay2
Highlight queries live in the grammar repository’s queries/ directory and need to be on your
runtimepath under queries/macaulay2/. The Nvim plugin ships folding,
indentation and tags queries; with its parser option pointing at a local checkout of the
grammar, it does the registration and the runtimepath part for you.
Setting up LSP
m2-ls is a standard language server, so any editor with an LSP client can use
it. In VS Code there’s nothing to set up: the extension brings the
server along.
Everywhere else, the recipe is the same:
- Put
m2-lson yourPATH(see Install). - Tell the editor to run
m2-ls(it talks over stdio) for.m2files. - Use the directory with
.git(or your package’s root) as the project root.
Settings can be passed either in initializationOptions or as workspace configuration under
the m2-ls section. They are in m2-ls settings.
| Editor | Guide |
|---|---|
| VS Code | VSCode extension |
| Neovim | Nvim |
| Emacs | Emacs |
| Helix, Zed, others | not written up yet. The three steps above are all there is to it |
Setting up LSP in Emacs
Untested. I don’t use Emacs, so this is the standard recipe rather than a verified setup. If you get it working (or it doesn’t), please let me know.
With eglot, built into Emacs 29+:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(M2-mode . ("m2-ls"))))
(add-hook 'M2-mode-hook #'eglot-ensure)
M2-mode is the major mode from Macaulay2’s own Emacs package; the server works alongside it.
Setting up LSP in Nvim
Neovim 0.11+ has everything built in, no nvim-lspconfig needed:
vim.filetype.add({ extension = { m2 = "macaulay2" } }) -- not needed with m2.nvim
vim.lsp.config("m2_ls", {
cmd = { "m2-ls" },
filetypes = { "macaulay2" },
root_markers = { ".git" },
})
vim.lsp.enable("m2_ls")
To change server settings, add them to the same call:
vim.lsp.config("m2_ls", {
-- ...
settings = {
["m2-ls"] = {
-- see the m2-ls configuration table
},
},
})
Check it’s running with :checkhealth vim.lsp while a .m2 file is open.
Inlay type hints are off by default in Neovim; turn them on with
vim.lsp.inlay_hint.enable(true).
ProceduralMacros
Macros for Macaulay2, written in Macaulay2: code that writes code before it runs. Inspired by Rust’s procedural macros, and still very much an experiment.
needsPackage "ProceduralMacros"
runSource ///
$defineRecord PackageRelease(makePackageRelease; releaseName, releaseVersion) $
///
turns into
PackageRelease = new Type of BasicList
makePackageRelease = (releaseName, releaseVersion) -> new PackageRelease from {releaseName, releaseVersion}
releaseName = value -> value#0
releaseVersion = value -> value#1
expandSource shows the expansion without running it. Two macros come built in:
defineRecord and inferReturns (which adds return types it can work out).
Your own
Declarative, a pattern and a template:
declMacro("double", "'e", "2 * ('e)")
'x is a metavariable, 'x:Number a typed one, '{…}+ repeats.
Or procedural, a function from the parsed input (a token tree from M2’s own parse) to new
tokens:
installMacro("myMacro", ts -> ...)
For bigger things there are typed syntax-tree views with visitors and folds, and
visualizeTree when you’re lost.
Install
installPackage "ProceduralMacros"
Needs M2 1.26.05+ (the first one with parse).
Watch out
load/needsdon’t expand macros, so usevalue openIn "file.m2"- templates don’t add parentheses for you
- the API will change
Prefer Rust? m2-syn does macros with a typed tree and quote-style templates.
m2-syn
syn for Macaulay2: a Rust crate that turns M2 code into a fully typed
syntax tree, lets you walk and rewrite it, and prints it back out. The
language server is built on it.
- Typed tree: a
SourceFileofCells, and each construct is its own type (Expr::Lambda,Expr::If, …), so matching on it is checked by the compiler. - Round trip: an untouched file prints back byte for byte, comments included.
Visit,VisitMut,Fold: override the node you care about, the rest is walked for you.quote_m2!/parse_quote_m2!: write M2 code with$(value)interpolation instead of gluing strings.
#![allow(unused)]
fn main() {
use m2_syn::nodes::Symbol;
use m2_syn::{ParsedFile, visit_mut::VisitMut};
struct Rename;
impl VisitMut for Rename {
fn visit_symbol_mut(&mut self, symbol: &mut Symbol) {
if symbol.text() == "left" {
symbol.set_text("renamed");
}
}
}
let file = ParsedFile::from_source("left + right")?.edit(|cst| {
Rename.visit_source_file_mut(cst);
});
assert_eq!(file.to_source(), "renamed+ right");
}
cargo add m2-syn
cargo run --example inspect -- file.m2 # in the repo: dump a file's tree
As a macro engine
The ProcMacro trait takes cells in and gives cells out, and with quote_m2! the output is
written as M2. Right now these macros run from Rust, not from inside M2; for macros you call
from M2 code, there’s ProceduralMacros.
The API is still moving between minor versions.
tree-sitter-macaulay2
The tree-sitter grammar: what editors use for highlighting, folding and indentation, and the parser at the bottom of everything Rust-side in this bundle.
It follows M2’s own precedence and newline rules, and part of the tests are generated from what
M2’s own parse says, so it sees the code the same way M2 does.
It comes with highlight, fold, indent, injection (TEST /// … /// bodies are highlighted as
M2) and tags queries, and bindings for Rust, Node and Python.
In your editor
In your code
cargo add tree-sitter tree-sitter-macaulay2
#![allow(unused)]
fn main() {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_macaulay2::language())?;
let tree = parser.parse("R = QQ[x, y]", None).unwrap();
}
Same name on npm and PyPI. If you’d rather have typed syntax nodes than a generic tree, use m2-syn.
m2-syn
syn for Macaulay2: a Rust crate that turns M2 code into a fully typed
syntax tree, lets you walk and rewrite it, and prints it back out. The
language server is built on it.
- Typed tree: a
SourceFileofCells, and each construct is its own type (Expr::Lambda,Expr::If, …), so matching on it is checked by the compiler. - Round trip: an untouched file prints back byte for byte, comments included.
Visit,VisitMut,Fold: override the node you care about, the rest is walked for you.quote_m2!/parse_quote_m2!: write M2 code with$(value)interpolation instead of gluing strings.
#![allow(unused)]
fn main() {
use m2_syn::nodes::Symbol;
use m2_syn::{ParsedFile, visit_mut::VisitMut};
struct Rename;
impl VisitMut for Rename {
fn visit_symbol_mut(&mut self, symbol: &mut Symbol) {
if symbol.text() == "left" {
symbol.set_text("renamed");
}
}
}
let file = ParsedFile::from_source("left + right")?.edit(|cst| {
Rename.visit_source_file_mut(cst);
});
assert_eq!(file.to_source(), "renamed+ right");
}
cargo add m2-syn
cargo run --example inspect -- file.m2 # in the repo: dump a file's tree
As a macro engine
The ProcMacro trait takes cells in and gives cells out, and with quote_m2! the output is
written as M2. Right now these macros run from Rust, not from inside M2; for macros you call
from M2 code, there’s ProceduralMacros.
The API is still moving between minor versions.
Work still in progress
Things I know are missing or half-done. If one of them is the reason you can’t use the bundle, tell me and it moves up the list.
- Prebuilt binaries of the command-line tools (outside VS Code, it’s
cargo installfor now) - One installer for the whole bundle instead of a
cargo installper tool - Emacs, Helix, Zed setup that someone has actually tested
- spectral-m2: interrupting a running computation
- m2-ls: comment-based lint suppression, scanning the whole project instead of only what you open
- Macros: calling m2-syn macros from M2 code, a stable ProceduralMacros API
- The Macaulay2 Book: needs actual chapters
- New documentation API: still a proposal
How to contribute
Something broke
Open an issue on the tool’s repository (links are at the bottom of each page). Helpful to
include: the tool and its version, your M2 version and OS, and a small .m2 snippet if you
have one.
“This was confusing” or “I expected X” counts as a bug too.
It worked
Tell me anyway! Knowing someone uses it, and for what, is the best motivation there is.
Code
Every tool lives in its own repository with its own README on building and testing. Good starting points:
- the grammar and m2-syn if you like parsers
- Emacs / Helix / Zed support if you use one of them, since I don’t
- the Work in progress list
Licenses
| m2-ls | MIT, except the embedded Macaulay2 documentation (GPL) |
| spectral-m2 | GPL-3.0 |
| mdbook-tsitter | MIT or Apache-2.0 |
| everything else | MIT |