Getting Started
This chapter takes you from nothing to a working setup: ShapeScript installed, a project with its own Python environment, VS Code showing shapes as you type, and a notebook running ShapeScript cells. Then it checks and runs a first program and calls it from Python. Later chapters explain everything you type here.
What you’ll set up
ShapeScript has two parts:
- a compiler and a language server, written in Rust: the
shapescriptandshapescript-lspprograms; - a Python package,
shapescript, with the runtime, the import hook and the notebook magic.
Both come prebuilt. pip install shapescript installs the Python package with the two programs inside it, for Linux, macOS and Windows, and the VS Code extension, from the Marketplace, brings its own copy of the language server. You need neither Rust nor Node.js; Building from source, at the end of the setup, is for other platforms, for versions that aren’t released, and for working on ShapeScript itself.
The commands are given for the usual shells: bash and zsh on macOS and Linux, fish, and PowerShell on Windows. A block of commands is marked with its shell when that matters: bash, zsh, fish or PowerShell. An unmarked block works in any of them, or, when a PowerShell block follows it, is the macOS and Linux version. If something goes wrong, Appendix D has the usual problems and their fixes.
Prerequisites
Python 3.9 or later
The runtime and PyTorch are Python packages, so you need Python. Any version from 3.9 works; 3.12 or 3.13 are the safest choice, since PyTorch has wheels for them everywhere. Check what you have:
python3 --version
py --version
If it’s missing or older than 3.9, install one:
- macOS: with Homebrew,
brew install python@3.13, which is/opt/homebrew/bin/python3.13; or the installer from python.org. - Linux: your distribution’s package; on Debian and Ubuntu you also need its
venvmodule:sudo apt install python3 python3-venv python3-pip. On Fedora,sudo dnf install python3. - Windows:
winget install Python.Python.3.13, or the installer from python.org. Usepyto run it: on Windows,pythonandpython3can open the Microsoft Store instead of running Python.
On a Mac with Apple Silicon (M1 and later), make sure the Python you use is an Apple Silicon one:
python3 -c "import platform; print(platform.machine())"
It should print arm64. An x86_64 Python, such as one from an old Intel Homebrew in /usr/local/bin, runs under Rosetta, and recent PyTorch doesn’t install in it. If python3 is such a Python, name Homebrew’s by its full path, /opt/homebrew/bin/python3.13, in the commands below.
VS Code, with the Python and Jupyter extensions
Install VS Code (brew install --cask visual-studio-code on macOS, winget install Microsoft.VisualStudioCode on Windows), then Microsoft’s Python extension, which brings Pylance and the Python debugger, and Jupyter, for notebooks. From the Extensions view, or:
code --install-extension ms-python.python
code --install-extension ms-toolsai.jupyter
On macOS, the code command exists once you’ve run Shell Command: Install ‘code’ command in PATH from VS Code’s Command Palette (Cmd+Shift+P). On Windows and Linux, VS Code’s installer adds it.
A project with a virtual environment
Each project gets a virtual environment: a folder, .venv, with its own Python and its own packages. It keeps one project’s PyTorch from breaking another’s, and it’s how VS Code, notebooks and the command line agree on which packages you have: they all use the same .venv.
Create and activate it
Make a folder for the project, and the environment in it:
mkdir ~/hello-shapescript
cd ~/hello-shapescript
python3 -m venv .venv
mkdir ~\hello-shapescript
cd ~\hello-shapescript
py -3.13 -m venv .venv
(On a Mac where python3 isn’t Apple Silicon, /opt/homebrew/bin/python3.13 -m venv .venv.)
Then activate it. Activating puts .venv’s python, pip and, later, shapescript first on your PATH, in this terminal only, so do it in each new terminal you open for the project:
source .venv/bin/activate
source .venv/bin/activate.fish
.venv\Scripts\Activate.ps1
(In Windows’ older cmd.exe, it’s .venv\Scripts\activate.bat. If PowerShell says running scripts is disabled, see Appendix D.) Your prompt now starts with (.venv). deactivate turns it off. Check that python is the environment’s:
python -c "import sys; print(sys.prefix)"
It prints the path of your .venv. Then update pip, since an old pip can miss the wheels newer packages are published as:
python -m pip install --upgrade pip
Install PyTorch
Install PyTorch before ShapeScript. ShapeScript depends on it, and pip keeps the build you installed; if you install ShapeScript first, pip picks the default build for you.
PyTorch comes in builds for CPUs, NVIDIA GPUs (CUDA) and Apple GPUs. pytorch.org’s selector gives the exact command for your machine; the usual ones are:
-
macOS, which includes Apple GPU support (
mps), and Linux with an NVIDIA GPU, which gets CUDA (a download of a few GB):pip install torch numpy einops -
Linux or Windows without a GPU, a much smaller download:
pip install torch --index-url https://download.pytorch.org/whl/cpu pip install numpy einops -
Windows with an NVIDIA GPU: the default build on Windows is CPU-only, so take the CUDA command from the selector, then
pip install numpy einops.
ShapeScript needs einops. PyTorch warns Failed to initialize NumPy without numpy, and notebooks use it, so install it too. Check what you got:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
True means PyTorch sees a CUDA GPU. On a Mac, torch.backends.mps.is_available() tells you about the Apple GPU.
Install ShapeScript
With the environment active, install ShapeScript from PyPI:
pip install shapescript
The current release is the beta 0.1.4b1. pip downloads the wheel for your platform, with the compiler and the language server inside it, and einops with it; it keeps the PyTorch you installed. Check all three:
python -c "import shapescript; print(shapescript.__version__)"
shapescript --version
shapescript-lsp --version
The first prints 0.1.4b1, and the two programs 0.1.4-beta.1, Rust’s spelling of the same version. If pip says it found no version, your platform has no wheel (see Building from source), or your Python is older than 3.9.
Updating, and more projects
To update ShapeScript, run pip install -U shapescript in each project’s environment. VS Code updates the extension by itself.
For another project, make a new folder with its own .venv, and install PyTorch and ShapeScript into it as above.
To start a ShapeScript package, something others pip install, shapescript new mypkg makes one; Chapter 11 explains packages. To build one, pip fetches ShapeScript from PyPI into a separate build environment, and PyTorch with it, which is a large download. To build with the environment’s own ShapeScript instead:
pip install hatchling
pip install --no-build-isolation ./mypkg
Setting up VS Code
The VS Code extension shows errors as you type, the shape of anything you hover over, drawn as a diagram, and a lot more (Chapter 13). Setting it up takes four steps.
1. Install the extension
In VS Code’s Extensions view (Cmd+Shift+X on macOS, Ctrl+Shift+X elsewhere), search for ShapeScript, by the publisher shapescript, and click Install. Or from a terminal:
code --install-extension shapescript.shapescript --pre-release
The extension is published as a pre-release for now, which --pre-release asks for. Its language server comes inside it, built for your platform: Linux (x64, arm64), macOS (Apple Silicon, Intel) or Windows (x64, arm64).
The extension isn’t on Open VSX, the registry that VSCodium, Cursor and some other editors use. There, download the .vsix for your platform from the Marketplace page and install it with Install from VSIX… in the Extensions view’s ⋯ menu.
2. Open the project folder
Open the project’s folder, not just a file: File › Open Folder…, or from a terminal:
code ~/hello-shapescript
VS Code keeps its settings and its choice of Python per folder. If it asks whether you trust the folder’s authors, say yes: in Restricted Mode, extensions are limited.
3. Select the environment’s Python
Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P elsewhere), run Python: Select Interpreter, and pick the one in .venv (VS Code labels it ('.venv': venv), and often offers it by itself). Everything else uses this choice: Pylance looks up imports in its packages, the ShapeScript language server finds installed ShapeScript packages and Python sources through it, the debugger and notebooks run in it, and new terminals in VS Code activate it.
To make the choice part of the project, put it in .vscode/settings.json in the project folder:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}
On Windows, the path is ${workspaceFolder}\\.venv\\Scripts\\python.exe.
4. Check that it works
The extension finds its language server inside itself, so there’s nothing to configure. (Without a bundled server, as in an extension built another way, it uses the shapescript.server.path setting, which takes a full path, such as the environment’s .venv/bin/shapescript-lsp, and then shapescript-lsp on the PATH.)
Then, once you’ve typed hello.ss from Hello, ShapeScript below, check that:
- The status bar, bottom right, says ShapeScript, and keywords such as
fnanddimare colored. - Hovering
xinnormalizeshows[n D]and a rectangle with its sides labelednandD; hoveringnormalizeshows its doc comment. - Changing
over=Dtoover=nunderlines the call in red within a second, and the Problems panel lists E0501. Changing it back clears it. - Once
hello.ssis saved without errors, the Explorer shows an arrow beside it, and under ithello.py. - In a Python file,
from hello import normalizeisn’t underlined, and hoveringnormalizeshows its ShapeScript signature.
That hello.py is written by the extension, for Python’s sake. Pylance doesn’t know about .ss files, so in a Python file, from hello import normalize would be Import "hello" could not be resolved. The generated module declares normalize with its ShapeScript signature, so Pylance resolves the import, completes the name and shows the shapes on hover; and when Python imports it, it loads hello.ss. The Explorer nests it under the .ss file to keep it out of the way. A .py file you wrote yourself is never replaced.
If the status bar says Plain Text, or hovers show nothing, see Appendix D.
Debugging
To stop at breakpoints in .ss files, click left of a line number in fn main() to set one, then run ShapeScript: Debug File from the Command Palette, or from the ▷ menu at the top right of the editor. It uses the Python debugger that came with the Python extension, in the interpreter you selected. Without the Python extension, install the debugger into the environment, pip install debugpy, and name the environment’s Python in the setting shapescript.debug.python. Chapter 13 has more.
Other editors
shapescript-lsp is an ordinary language server, so any editor with LSP support can use it; with the environment active, it’s on your PATH, since the shapescript package installed it there. editors/neovim/shapescript.lua in the repository is a minimal Neovim setup.
Building from source
The published wheels and extension cover Linux, macOS and Windows on the usual processors. To run ShapeScript elsewhere, to try a commit that isn’t released, or to work on ShapeScript itself, build it from the repository instead. Everything else in this chapter stays the same: only Install ShapeScript and Install the extension change.
Prerequisites: Rust
You need Rust 1.88 or later. Install it with rustup, which also keeps it up to date.
On macOS and Linux:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Rust needs a C linker. On macOS, install Apple’s command line tools with xcode-select --install; on Debian and Ubuntu, sudo apt install build-essential; on Fedora, sudo dnf install gcc.
On Windows:
winget install Rustlang.Rustup
On Windows, Rust links with Microsoft’s C++ build tools. If you don’t have Visual Studio, the installer offers to install them; or install them yourself with winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended".
Open a new terminal, so it sees the new PATH, and check:
cargo --version
In fish, if cargo isn’t found, add Rust’s folder to your PATH once (fish remembers it):
fish_add_path ~/.cargo/bin
If the version is older than 1.88, rustup update updates it.
Prerequisites: Git
You clone ShapeScript with Git. macOS has it once the command line tools are installed; on Linux, it’s the git package; on Windows, winget install Git.Git. Git for Windows also installs Git Bash, which you’ll use to build the VS Code extension.
Prerequisites: Node.js
Building the extension needs Node.js 20 or later: brew install node on macOS, winget install OpenJS.NodeJS.LTS on Windows, and nodejs.org’s packages on Linux (a distribution’s own can be older than 20). Check with node --version.
Clone and build
Clone the repository into your home folder and build the compiler and the language server. The commands are the same in every shell:
cd ~
git clone https://gitlab.com/NatanFreeman/shapescript.git
cd shapescript
cargo build --release -p shapescript-cli -p shapescript-lsp
The first build takes a few minutes. It writes shapescript and shapescript-lsp (with .exe on Windows) to target/release.
Now copy the two programs into the Python package, which is what the published wheels do. Installing the package then gives each virtual environment its own shapescript and shapescript-lsp commands, so you don’t need to put target/release on your PATH:
mkdir -p crates/py/python/shapescript/bin
cp target/release/shapescript target/release/shapescript-lsp crates/py/python/shapescript/bin/
New-Item -ItemType Directory -Force crates\py\python\shapescript\bin
Copy-Item target\release\shapescript.exe, target\release\shapescript-lsp.exe crates\py\python\shapescript\bin\
(Git ignores that folder.) If you also want the commands outside any virtual environment, for example for an editor other than VS Code, install them into Rust’s own folder, which rustup put on your PATH:
cargo install --path crates/cli --locked
cargo install --path crates/lsp --locked
shapescript run and shapescript export need a Python with PyTorch and ShapeScript. Run from a virtual environment’s shapescript, they use its Python; run from elsewhere, they use --python, or the SHAPESCRIPT_PYTHON environment variable, or python3 (Appendix D shows how to set it).
Install the package
In your project’s environment, after PyTorch, install the package from your clone instead of from PyPI:
pip install ~/shapescript/crates/py
pip install $HOME\shapescript\crates\py
pip compiles the package’s Rust part, which takes a minute, and installs it with the two programs you copied in. If shapescript --version then says this installation of shapescript has no shapescript binary, the copy step above was skipped: do it, and run pip install again.
If you’re working on ShapeScript itself, an editable install is handier: your changes to its Python code take effect without reinstalling (changes to its Rust need the command again). With the environment active:
pip install maturin
cd ~/shapescript/crates/py
maturin develop --release
To use ShapeScript, pip install is simpler: it doesn’t need maturin, and works inside conda, which maturin develop refuses (see Appendix D).
To update, run git pull in ~/shapescript, then the build, the copy and the pip install again, and rebuild the extension (below), so that the compiler, the runtime and the editor come from the same version.
Build the extension
The extension you build is a .vsix file, with the language server you built inside it. Name your platform: darwin-arm64 (a Mac with Apple Silicon), darwin-x64 (an Intel Mac), linux-x64, linux-arm64, win32-x64 or win32-arm64. On macOS and Linux:
cd ~/shapescript/editors/vscode
npm ci
cd ~/shapescript
ci/vsix.sh darwin-arm64
code --install-extension dist/vsix/shapescript-darwin-arm64.vsix
npm ci downloads the extension’s dependencies, and ci/vsix.sh builds it and packs it with target/release/shapescript-lsp. On Windows, run the same commands in Git Bash, with win32-x64 (or win32-arm64) for the platform: ci/vsix.sh is a bash script. You can also install the file from VS Code: in the Extensions view, the ⋯ menu has Install from VSIX…. Reload any open VS Code window afterwards (Developer: Reload Window in the Command Palette).
If you’re working on ShapeScript itself, you don’t need the .vsix: open the ~/shapescript folder in VS Code and press F5. VS Code builds the language server and the extension, then opens examples/ in a new window with the extension loaded.
The shapescript command
The command has a subcommand for each job. You’ll meet them all in this book:
shapescript check model.ss # check types and shapes; print errors and warnings
shapescript explain E0401 # explain an error code
shapescript build model.ss -o model.py # compile to Python
shapescript run program.py # run a program that imports .ss files
shapescript export model.ss --fn f # a torch.export program (Chapter 15)
shapescript export model.ss --hf out # a Hugging Face model, for transformers and vLLM (Chapter 15)
shapescript new my_layers # a package for pip (Chapter 11)
Hello, ShapeScript
In your project folder, create a file called hello.ss:
devices cpu
dim D = 4
## Turns each row of scores into probabilities.
fn normalize(x: [n D]) -> [n D] Prob over D = softmax(x, over=D)
fn main():
x = 3 |> map(i => D |> map(j => j to f32))
print("hello from ShapeScript")
print(normalize(x))
Line by line:
devices cpusays where the program’s tensors live. Every program says so once, and there’s no default. You’d writedevices cuda[0]for a GPU, ordevices mpson a Mac.dim D = 4declares a constant size,D, which types can use.## ...is a doc comment. Your editor shows it when you hover overnormalize.fn normalize(x: [n D]) -> [n D] Prob over Ddeclares a function that takes a tensor with two axes, any numbernof rows andDcolumns, and returns one of the same shape whose values are probabilities that sum to 1 overD. The body after=is a single expression.fn main()is where a program run withshapescript runstarts. Its body is indented, as in Python.3 |> map(i => D |> map(j => j to f32))builds a[3 D]tensor: for each of 3 rows, the indices0to3alongDas floats.|>passes a value to the next step, andmapcomputes its function,j => j to f32, for every index at once; Chapter 9 covers it.
Check it:
shapescript check hello.ss
It prints nothing: no news is good news. Now make a mistake. Change over=D to over=n, which is the wrong axis for normalizing rows:
devices cpu
dim D = 4
fn normalize(x: [n D]) -> [n D] Prob over D = softmax(x, over=n)
$ shapescript check hello_bad.ss
error[E0501]: expected `Prob over D`, got `Prob over n`
--> hello_bad.ss:5:47
|
5 | fn normalize(x: [n D]) -> [n D] Prob over D = softmax(x, over=n)
| ^^^^^^^^^^^^^^^^^^ this is `[n D] Prob over n`, but the return type is `[n D] Prob over D`
|
= help: convert logits with `to Prob over D`, or if these are already probabilities, say so with `as Prob over D`
For more information about E0501, run `shapescript explain E0501`.
Read the message from the top. It names the problem, points at the line, shows what was expected and what was found, and ends with a code you can look up. You’ll see many messages like it in this book.
Change it back and run the program:
shapescript run hello.ss
hello from ShapeScript
tensor([[0.0321, 0.0871, 0.2369, 0.6439],
[0.0321, 0.0871, 0.2369, 0.6439],
[0.0321, 0.0871, 0.2369, 0.6439]])
shapescript run runs a .ss file’s fn main(), or a Python program, in the Python it finds: the active virtualenv’s, or the one given with --python.
Calling ShapeScript from Python
Most of the time you’ll call ShapeScript from Python, from a training script or a notebook. A .ss file is a module Python can import, once the import hook is installed:
import torch
import shapescript
shapescript.install() # lets `import` find .ss files
from hello import normalize # compiles hello.ss, and caches the result
print(normalize(torch.randn(5, 4)))
The imported normalize is a Python function. It checks its argument when it’s called, so normalize(torch.randn(5, 3)) raises a shapescript.ShapeError naming hello.ss and the line, instead of computing garbage. Tracebacks point into the .ss file.
shapescript run program.py runs a Python program with the hook already installed, so a script run that way doesn’t need shapescript.install().
The editor also writes a small Python module beside each .ss file, hello.py beside hello.ss, with typed declarations of its functions, so Pylance and mypy know about them too. Chapter 13 has the details.
Notebooks
ShapeScript runs in Jupyter notebooks through a magic command. A notebook’s kernel is the Python that runs its cells, and it has to be your environment’s Python, which has ShapeScript. Install the kernel package into the environment, with it active:
pip install ipykernel
In VS Code, create a notebook (Create: New Jupyter Notebook in the Command Palette, or a new file ending in .ipynb), click Select Kernel at the top right, choose Python Environments, and pick .venv. (If you skipped pip install ipykernel, VS Code offers to install it.)
In Jupyter in the browser, install it into the same environment and start it from the project folder:
pip install jupyterlab
jupyter lab
Its kernel, Python 3 (ipykernel), is then the environment’s Python.
In the notebook’s first cell, load the extension once:
%load_ext shapescript
Then any cell that starts with %%shapescript is ShapeScript:
%%shapescript
devices cpu
dim D = 4
fn normalize(x: [n D]) -> [n D] Prob over D = softmax(x, over=D)
Run both. The second prints nothing: it only declares things, and they check. Chapter 14 is about notebooks.
Exercises
- Run
shapescript explain E0501. What does the reference say the code means? - In
hello.ss, change the return type ofnormalizeto-> [D n] Prob over D. Predict whether the checker accepts it, then check. - Change
dim D = 4todim D = 0. What does the checker say?
Solutions
-
E0501 is a value that doesn’t fit the domain where it’s used: here,
softmax(x, over=n)gives probabilities overn, and the function promised probabilities overD. -
It’s an error.
softmaxkeeps the shape,[n D], and the return type promises[D n]: the axes are matched by position, sonwould have to equalD, andnis chosen by the caller:fn normalize(x: [n D]) -> [D n] Prob over D = softmax(x, over=D) -
A dim must be a whole number of at least 1, so
dim D = 0is error E0305:dim D = 0