The Scaly Programming Language

Chapter 2

Tutorial

This chapter is a hands-on introduction to Scaly. It gets you from an empty file to a running program, then takes you on a quick tour of the language. Later chapters cover everything here in depth; the goal now is to write and run real code as fast as possible.

Getting Started

To try Scaly before installing anything, a browser is enough. The tour at scaly.io/play/tour.html runs the programs of A Tour of Scaly step by step, and you can change them; the REPL at scaly.io/play is the interactive session of Trying Things Out. The compiler runs in the browser tab itself: nothing is sent anywhere.

Installing Scaly

Scaly is installed by one command. On macOS and Linux, in a terminal:

curl -fsSL https://scaly.io/install.sh | sh

On macOS 26.4 and newer, Terminal stops a pasted command that downloads something with a warning, Possible Malware, Paste Blocked — this one too. Choose Paste Anyway; the script it runs can be read first at scaly.io/install.sh.

On Windows, in PowerShell (Scaly on Windows goes through the installation there step by step, and through working with VS Code and Visual Studio):

irm https://scaly.io/install.ps1 | iex

Open a new terminal afterwards. The installer has put three commands on your PATH: scaly, the tool you will use most, scalyc, the compiler by itself, and scalyls, the language server your editor talks to. Everything else it installed lives in ~/.scaly. Installation has the details: which systems get Scaly ready to run, what the others need, how to ask for a particular version, and how to remove it again.

Hello, World!

A Scaly program is just a file of statements — there is no main function to declare and nothing to import. Put this single line in a file called hello.scaly:

print "Hello, World!"

Run it:

scaly run hello.scaly

The program prints:

Hello, World!

scaly run compiles the file in memory and runs it at once; nothing is left on disk. To get a program you can keep and hand to someone else, build it:

scaly build hello.scaly -o hello
./hello

Add --release for the optimised build: the whole program — your code, the standard library and every package it uses — is optimised as one piece.

scaly new hello writes such a start for you: a directory hello with hello.scaly in it — a function, a test for it (scaly test hello.scaly runs every function whose name begins with test) and the line that prints.

That is the whole edit-compile-run loop. Everything that follows is just more interesting things to put between the braces.

Trying Things Out

scaly without arguments starts an interactive session. An expression shows its value and its type; a let or var keeps a value for the entries that follow:

$ scaly
scaly> 6 * 7
it: int = 42
scaly> let name "Scaly"
name: String = "Scaly"
scaly> :where name
  name: String
    the value   8 bytes, on the session page of its entry
    its text    5 bytes, on a page of its own
scaly> :quit

:where shows where a value lives — Scaly manages memory in regions, and here you can watch it do so. :help lists the other commands.

Using Scaly with Visual Studio Code on macOS and Linux

On Windows, follow Scaly on Windows instead: the installation is different there.

You can drive the whole edit-compile-run-debug loop from Visual Studio Code. The Scaly extension provides syntax highlighting and connects the editor to the scalyls language server you installed in Installing Scaly — giving live error squiggles, hover, go-to-definition, completion, rename, and more.

If you do not have VS Code yet, download it from code.visualstudio.com (installers for macOS and Linux; on Ubuntu, sudo snap install code --classic also works).

Installing the Scaly Extension

The Scaly extension is distributed as a .vsix package on scaly.io. Download scaly.io/downloads/scaly-vscode.vsix, then install it from within VS Code: open the Extensions view, click the ··· menu in its title bar, choose Install from VSIX…, and pick the downloaded file.

The extension launches scalyls from your PATH, so install the toolchain first (Installing Scaly). If VS Code was already running when you installed the toolchain, restart it (not just the window) so its process picks up the updated PATH. Should the server binary live somewhere unusual, point the scaly.server.path setting at it.

What the Language Server Gives You

Open any folder, create a file double.scaly, and type a small program:

function double(n: int) returns int
    n * 2

let d double(21)
if d = 42
    print "doubled!"

As you type, the language server is at work. Things to try:

  • Diagnostics — misspell a call (say, change double(21) to doubel(21)): a red squiggle appears and the Problems panel shows function not found: doubel. Fix it and the squiggle clears.

  • Hover — rest the mouse on double or a binding to see its signature or type.

  • Go to Definition (F12) and Find All References (Shift+F12) — work across files and into the standard library.

  • Completion — names in scope and members appear as you type.

  • Rename (F2) — renames a symbol everywhere it is used, shadow-aware.

  • Outline and symbols — the Outline view and Cmd+Shift+O list the file's functions and types; Cmd+T searches symbols across the workspace. (Use Ctrl instead of Cmd on Linux.)

  • Signature help — parameter hints while you type a call; the hover over a construction also tells where it is allocated, on the stack or in which region; semantic highlighting and folding come for free.

If the server ever needs a kick — say, after you rebuilt it — run Scaly: Restart Language Server from the Command Palette.

Writing Scaly with GitHub Copilot

An AI assistant has seen little Scaly. Left to itself it writes what looks like Rust or Swift, and Scaly differs exactly there: = compares and := assigns, not negates, a function cannot print. Two things make it write Scaly: a file that teaches it the language, and the compiler, whose messages it reads.

Put the language reference for assistants into your folder, in a terminal at the folder's root:

mkdir -p .github
curl -fsSL https://scaly.io/AGENTS.md -o .github/copilot-instructions.md

GitHub Copilot reads .github/copilot-instructions.md with every chat request in this folder. Other assistants read the same text under the name AGENTS.md at the folder's root.

Open the Chat view, set its mode to Agent, and say what you want — say, Write a Scaly program that counts the words of the files named on its command line, with a test for the counting function. In agent mode Copilot writes the files, runs scaly run and scaly test in the terminal, and reads the errors the language server reports in the Problems panel. Scaly's messages name the line and usually the fix, and Copilot corrects itself from them. Let it run the program and the tests before you accept what it wrote.

Run, Build and Test

The extension offers three tasks for the file in the active editor, with no tasks.json to write. Terminal > Run Task… lists them under scaly:

scaly: run

scaly run: the program is compiled in memory and runs in the terminal panel.

scaly: build

scaly build -g: the program, with debug information, beside the file. ⇧⌘B (Ctrl+Shift+B) offers it.

scaly: test

scaly test: the file's tests.

A compiler error lands in the Problems panel, a click away from its line. To change a task — another output, an argument —, choose Terminal > Configure Tasks… and pick it: VS Code writes it into .vscode/tasks.json, where it can be edited.

Starting the Debugger

Debugging needs nothing more. The extension drives lldb-dap, the debugger that comes with Apple's Command Line Tools (the installer fetches them where they are missing), with Xcode, and with LLVM — on Ubuntu, sudo apt install lldb-21. Open a .scaly file and press F5: the extension builds it with debug information and starts it under the debugger. Should lldb-dap live somewhere unusual, point the scaly.debugAdapter.path setting at it.

For a configuration of your own — arguments, an environment, another program — you write a .vscode/launch.json, and that needs a folder open (File > Open Folder…), not just a file. Open the Run and Debug view: the icon with the play triangle and the bug in the Activity Bar on the left, or ⇧⌘D (Ctrl+Shift+D). As long as the folder has no launch.json, the view shows a Run and Debug button — the same as F5 — and beneath it the link create a launch.json file. Click the link with a .scaly file in the editor; should VS Code ask which debugger, choose Scaly. It writes the file, opens it, and what it writes debugs the open file:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "scaly",
            "request": "launch",
            "name": "Debug the open Scaly file",
            "source": "${file}"
        }
    ]
}

source is built before the launch; program names an executable to debug as it is, and args, env and cwd go to the program.

Arguments and the Environment shows a configuration with arguments and an environment.

Breakpoints in Scaly Files

VS Code only lets you set breakpoints in files whose language an installed extension has declared debuggable. The Scaly extension from Installing the Scaly Extension does exactly that for .scaly files, so with it installed, clicking in the editor gutter places a breakpoint (a red dot appears) — nothing to configure.

Setting a Breakpoint and Stepping

Put this program in area.scaly in your workspace folder. It computes a rectangle's area from two mutable bindings — small, but enough to stop and inspect some values:

var width 8
var height 5
var area width * height
if area = 40
    print "the area is 40"
else
    print "unexpected area"

Click in the gutter to the left of the var area width * height line to set a breakpoint (a red dot appears), then press F5. The program builds and runs, and execution stops at that line before it executes — at which point width and height already have values but area does not yet.

With execution paused you can:

  • Read width and height in the Variables pane, or hover them in the editor to see their values.

  • Step one line at a time with F10 (Step Over) — after stepping past the breakpoint line, area appears with the value 40. Step into a called function with F11.

  • Press F5 again to continue to the program's end.

Breakpoints, stepping, and variable inspection all rely on the -g debug information; without it the program still runs, but the debugger has no source lines to stop on.

Stepping into Functions and the Call Stack

Stepping becomes more interesting once a function is involved. F10 (Step Over) runs a call as a single step; F11 (Step Into) descends into the called function so you can watch it run. Put this program in rectangle.scaly in your workspace folder — it factors the area calculation out into a function:

function area(w: int, h: int) returns int
{
    var result w * h
    result
}

var width 8
var height 5
var a area(width, height)
if a = 40
    print "the area is 40"
else
    print "unexpected area"

Set a breakpoint on the var a area(width, height) line and press F5. When execution stops there, width and height show in the Variables pane. Now press F11 instead of F10: the editor jumps into the body of area and stops on var result w * h.

The Call Stack pane (bottom-left of the Run and Debug view) now lists two frames — area on top and main below it — showing exactly how you got here. Click a frame to switch the Variables pane and the editor to that frame's context: selecting main shows width and height again, while area shows its own local result (still unassigned at this point, just as area was before its line ran in the previous example).

From inside the function you can keep stepping, press Shift+F11 (Step Out) to return to the caller, or F5 to continue to the end. The function-and-procedure model is covered in Functions and Procedures and Mutation.

Arguments and the Environment

Put this program in greet.scaly in your workspace folder. It greets everyone named on its command line, with the greeting the environment variable GREETING holds, and Hello where there is none:

var greeting os.environment_variable("GREETING")
if greeting.length = 0
    greeting := "Hello"
for name in os.arguments()
    print "`greeting`, `name`!"

os.arguments() answers the arguments the program was started with, its own name left out; os.environment_variable reads a variable of the environment, and is empty where it is not set. In a terminal:

$ GREETING=Hi scaly run greet.scaly Ada Grace
Hi, Ada!
Hi, Grace!

Under the debugger, the arguments and the environment come from .vscode/launch.json (Starting the Debugger says how to create one). A launch.json may hold several configurations; the drop-down at the top of the Run and Debug view chooses the one F5 starts. Here the first builds greet.scaly and runs it with two arguments, a variable in its environment and the workspace folder as its working directory; the second debugs a program that is already built — with scaly build -g, or there are no line tables for a breakpoint to bind to:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "scaly",
            "request": "launch",
            "name": "Debug greet with arguments",
            "source": "${workspaceFolder}/greet.scaly",
            "args": ["Ada", "Grace"],
            "env": { "GREETING": "Hello" },
            "cwd": "${workspaceFolder}"
        },
        {
            "type": "scaly",
            "request": "launch",
            "name": "Debug the built server",
            "program": "${workspaceFolder}/build/server",
            "args": ["--port", "8080"]
        }
    ]
}

Choose Debug greet with arguments in the drop-down, set a breakpoint on the print line and press F5. The program stops there with name "Ada" in the Variables pane; F5 runs the next round, with "Grace". What the program prints appears in the Debug Console.

What the Debugger Shows

For whichever stack frame is selected:

  • Breakpoints land on top-level statements, bindings and function bodies, and line by line inside loop and conditional bodies — on a single statement within a while, for, if or choose, not just on the statement around it.

  • The Variables pane shows a function's arguments and its let and var bindings: numbers and booleans; structures, unions and pointers with their fields to expand, a union's variant by its name; a String as its text; a Vector, Array or List with its elements.

  • The Watch view and Debug Console evaluate expressions as C/C++ and do not understand Scaly names. To read a value, use the Variables pane or hover over the name in the editor.

Scaly on Windows

Everything in this chapter works on Windows 10 and 11, on x64 and on Arm. Choose how you want to work: in Visual Studio Code (Using Scaly with Visual Studio Code on Windows) or in Visual Studio (Using Scaly with Visual Studio).

Using Scaly with Visual Studio Code on Windows

The following sections describe how to use Visual Studio Code to develop Scaly programs.

Installing Scaly, VS Code and the Extensions

  1. Install Scaly. Click Start, type PowerShell and press Enter. In the window that opens, paste this line (a right-click pastes) and press Enter:

    irm https://scaly.io/install.ps1 | iex

    Along with Scaly, the installer sets up Microsoft's C++ build tools, which Scaly needs to build programs. This download is several gigabytes and takes a few minutes. When Windows asks whether to allow the installation, answer Yes. When the Scaly installer ends with scaly build: ok, everything is in place. Close PowerShell.

  2. Install VS Code: download the installer from code.visualstudio.com and run it. If VS Code was installed and running already, close every one of its windows now, so that it starts again and finds Scaly.

  3. Download the Scaly extension, scaly.io/downloads/scaly-vscode.vsix.

  4. Start VS Code and open the Extensions view: the icon with the four squares in the Activity Bar on the left, or Ctrl+Shift+X. Click the ··· menu in the view's title bar, choose Install from VSIX…, and pick scaly-vscode.vsix from your Downloads folder.

  5. In the same view, type C/C++ into the search box at the top. Choose the extension named C/C++ whose publisher is Microsoft (its identifier is ms-vscode.cpptools) and click Install; it brings the debugger. If you skip this step, the first F5 asks for it with a button Install C/C++.

The Scaly extension launches scalyls and scaly from your PATH; should they live somewhere unusual, point the scaly.server.path and scaly.tool.path settings at them.

Checking the Language Server

  1. Make a folder for this tutorial: choose File > Open Folder…, go to Documents, click New folder, name it scaly-tutorial, double-click it so that the dialog shows its (still empty) inside, and click Select Folder.

  2. Trust the folder. VS Code opens a new folder in Restricted Mode, where it starts no language server and no debugger; a bar at the top of the window says so. Click Manage in that bar, then Trust. (Should VS Code ask in a dialog instead, answer Yes, I trust the authors.)

  3. Choose File > New File…, save it right away (Ctrl+S) as double.scaly in that folder, and type:

    function double(n: int) returns int
        n * 2
    
    let d double(21)
    if d = 42
        print "doubled!"

    The words function, returns and let are coloured: the extension knows the file.

  4. Change double(21) to doubel(21). A red squiggle appears under it, and the Problems panel (Ctrl+Shift+M) shows function not found: doubel: the language server is running. Change it back; the squiggle clears.

  5. Rest the mouse on double to see its signature, press F12 on it to go to its definition and Shift+F12 to list its uses, F2 to rename it everywhere. Ctrl+Shift+O lists the file's functions and types, Ctrl+T searches symbols across the folder.

What the Language Server Gives You lists what else the language server does. If the squiggle never appears, VS Code did not find the language server: close all of its windows and start it again.

To write Scaly with GitHub Copilot, follow Writing Scaly with GitHub Copilot; in PowerShell the reference for assistants is fetched with:

mkdir -Force .github
irm https://scaly.io/AGENTS.md -OutFile .github\copilot-instructions.md

Run, Build and Test

With double.scaly in the editor, choose Terminal > Run Task…, then scaly. Three tasks are offered:

scaly: run

scaly run: the program runs in the terminal panel at the bottom and prints doubled!.

scaly: build

scaly build -g: writes double.exe, the program, and double.pdb, its debug information, beside the file. Ctrl+Shift+B offers it too.

scaly: test

scaly test: the file's tests.

A compiler error lands in the Problems panel, a click away from its line. Should scaly: build say that it has nothing to link with, the C++ build tools are missing: run the Scaly installer again (Installing Scaly, VS Code and the Extensions) and answer Yes when Windows asks. To change a task, choose Terminal > Configure Tasks… and pick it: VS Code writes it into .vscode\tasks.json, where it can be edited.

Setting a Breakpoint and Stepping

  1. Create area.scaly in the folder, as double.scaly above, with this program:

    var width 8
    var height 5
    var area width * height
    if area = 40
        print "the area is 40"
    else
        print "unexpected area"
  2. Click in the gutter to the left of the line number of var area width * height. A red dot appears: a breakpoint. (F9 sets one on the line the cursor is in.)

  3. Press F5. Should VS Code ask which debugger to use, choose Scaly. The extension builds area.exe and area.pdb, then starts the program under the Visual Studio debugger. Should a message say that the C/C++ extension is missing, click Install C/C++, wait until it is installed, and press F5 again.

  4. The program stops at the breakpoint, before the line runs; the line is highlighted in yellow. The Run and Debug view on the left shows the Variables pane: width is 8, height is 5, area has no value yet. Resting the mouse on a name in the editor shows its value too.

  5. Press F10 (Step Over): the line runs, and area shows 40.

  6. Press F5 again: the program runs to its end. What it prints, the area is 40, appears in the Debug Console (Ctrl+Shift+Y).

Every F5 builds the file again, so a change to the source is always in the program you debug.

Stepping into Functions and the Call Stack

  1. Create rectangle.scaly in the folder, with the area calculation in a function:

    function area(w: int, h: int) returns int
    {
        var result w * h
        result
    }
    
    var width 8
    var height 5
    var a area(width, height)
    if a = 40
        print "the area is 40"
    else
        print "unexpected area"
  2. Set a breakpoint on var a area(width, height) and press F5. The program stops there.

  3. Press F11 (Step Into): the editor jumps into the body of area and stops on var result w * h.

  4. The Call Stack pane, in the Run and Debug view below Variables, lists two frames: area on top, main below it, each with the program's name in front (rectangle.exe!). Click main: the Variables pane shows width and height. Click area: it shows w, h and result, still without a value.

  5. Press Shift+F11 (Step Out) to return to the caller, then F5 to run to the end.

Arguments and the Environment

A program that is started with arguments and an environment gets them under the debugger from a .vscode\launch.json.

  1. Create greet.scaly in the folder, as double.scaly above, with this program. It greets everyone named on its command line, with the greeting the environment variable GREETING holds, and Hello where there is none:

    var greeting os.environment_variable("GREETING")
    if greeting.length = 0
        greeting := "Hello"
    for name in os.arguments()
        print "`greeting`, `name`!"

    os.arguments() answers the arguments the program was started with, its own name left out; os.environment_variable reads a variable of the environment, and is empty where it is not set.

  2. Open the Run and Debug view: the icon with the play triangle and the bug in the Activity Bar, or Ctrl+Shift+D.

  3. Click the link create a launch.json file below the Run and Debug button. Should VS Code ask which debugger, choose Scaly. It writes .vscode\launch.json and opens it.

  4. Replace its content with this and save it (Ctrl+S):

    {
        "version": "0.2.0",
        "configurations": [
            {
                "type": "scaly",
                "request": "launch",
                "name": "Debug greet with arguments",
                "source": "${workspaceFolder}/greet.scaly",
                "args": ["Ada", "Grace"],
                "env": { "GREETING": "Hello" },
                "cwd": "${workspaceFolder}"
            },
            {
                "type": "scaly",
                "request": "launch",
                "name": "Debug the open Scaly file",
                "source": "${file}"
            }
        ]
    }

    The first configuration builds greet.scaly and runs it with two arguments, a variable in its environment and the folder as its working directory. The second is what F5 did so far: it builds and debugs the file in the editor.

  5. Choose Debug greet with arguments in the drop-down at the top of the Run and Debug view.

  6. Open greet.scaly, set a breakpoint on the print line and press F5. The program stops there; the Variables pane shows name as "Ada".

  7. Press F5: the next round stops with "Grace". Press F5 once more to run to the end. The Debug Console shows Hello, Ada! and Hello, Grace!.

A configuration may also debug a program built beforehand: "program": "${workspaceFolder}/greet.exe" in place of source runs it as it is (built with -g, or there is nothing for a breakpoint to bind to).

What the Debugger Shows

  • Breakpoints land on top-level statements, bindings and function bodies, and line by line inside loop and conditional bodies.

  • The Variables pane shows a function's arguments and its let and var bindings: numbers and booleans; structures, unions and pointers with their fields to expand; a String as its text; a Vector, Array or List with its elements. A generic type is written as in C++, Vector<int>.

  • The Watch view and the Debug Console evaluate expressions as C++ and do not understand Scaly. To read a value, use the Variables pane or hover over the name in the editor.

Using Scaly with Visual Studio

Visual Studio debugs a Scaly program with the debugger it uses for C++. It has no Scaly project type and does not know the language yet: it works on a folder, builds through a task you write once, and shows a .scaly file as plain text. For highlighting, completion and the rest of the language server, edit in VS Code (Using Scaly with Visual Studio Code on Windows); both can have the same folder open at once.

Installing Visual Studio and Scaly

Visual Studio comes first: Scaly builds programs with its C++ tools, and when it finds them it installs nothing of its own.

  1. Download Visual Studio 2026 from visualstudio.microsoft.com; the free Community edition will do. Run the installer. If Visual Studio 2026 is installed already, start the Visual Studio Installer from the Start menu instead and click Modify.

  2. On the Workloads tab, tick Desktop development with C++. It brings the C++ debugger and the C++ tools Scaly builds with.

  3. On a PC with an Arm processor (a Snapdragon, for instance; Settings > System > About says ARM-based processor), the tools for Arm must be ticked as well: in the Installation details on the right, under Desktop development with C++, tick the entry for C++ ARM64/ARM64EC build tools. On any other PC, skip this step.

  4. Click Install (or Modify) and wait until the installer is done. Should it ask you to restart your PC, do so.

  5. Install Scaly. Click Start, type PowerShell and press Enter. In the window that opens, paste this line (a right-click pastes) and press Enter:

    irm https://scaly.io/install.ps1 | iex

    The installer finds the C++ tools of Visual Studio and fetches nothing more. When it ends with scaly build: ok, everything is in place. Close PowerShell.

  6. Start Visual Studio. If it was running while Scaly was installed, close it and start it again, so that it finds Scaly.

Opening the Folder

  1. In Visual Studio's start window, choose Open a local folder; with Visual Studio already open, choose File > Open > Folder… and go to Documents. If there is no scaly-tutorial yet from Using Scaly with Visual Studio Code on Windows, click New folder and name it scaly-tutorial. Double-click scaly-tutorial, so that the dialog shows its inside, and click Select Folder.

  2. The Solution Explorer lists the folder's files (View > Solution Explorer, or Ctrl+Alt+L, should it not be there).

  3. If area.scaly is there already, from Setting a Breakpoint and Stepping, double-click it. Otherwise right-click the folder at the top of the Solution Explorer, choose Add > New File…, name it area.scaly, type this program, and save it with Ctrl+S:

    var width 8
    var height 5
    var area width * height
    if area = 40
        print "the area is 40"
    else
        print "unexpected area"

Building

A task tells Visual Studio how to build a .scaly file. It is written once per folder:

  1. Right-click area.scaly in the Solution Explorer and choose Configure Tasks. Visual Studio creates .vs\tasks.vs.json and opens it.

  2. Replace all of its content with this task, which builds the file with debug information, and save it (Ctrl+S):

    {
        "version": "0.2.1",
        "tasks": [
            {
                "taskLabel": "scaly build",
                "appliesTo": "*.scaly",
                "type": "launch",
                "contextType": "build",
                "command": "scaly",
                "args": [ "build", "-g", "${file}",
                          "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ]
            }
        ]
    }
  3. Right-click area.scaly again: the menu now has Build. Choose it. The compiler's messages appear in the Output window (View > Output), and area.exe and area.pdb appear beside the source. Should the Solution Explorer not show them yet, click its Refresh button.

The task applies to every .scaly file in the folder: each one's menu has Build, and each builds into an .exe of its own name.

Setting a Breakpoint and Stepping

  1. Right-click area.exe in the Solution Explorer and choose Set as Startup Item. The start button in the toolbar (the green triangle) now says area.exe.

  2. In area.scaly, click in the grey margin left of var area width * height, or put the cursor on that line and press F9. A red dot appears: a breakpoint.

  3. Press F5. A console window opens for the program, and Visual Studio stops at the breakpoint, before the line runs; a yellow arrow marks the line.

  4. The Locals window at the bottom shows the bindings: width is 8, height is 5. (If it is not there: Debug > Windows > Locals.) Resting the mouse on a name in the editor shows its value too.

  5. Press F10 (Step Over): the line runs, and area shows 40.

  6. Press F5 again: the program runs to its end and prints the area is 40 into its console window.

Two things to know. The console window closes when the program ends; to read its output afterwards, untick Tools > Options > Debugging > Automatically close the console when debugging stops. And F5 runs the .exe as it is: after changing the source, choose Build again first. Should Visual Studio refuse a breakpoint in the .scaly file, start with F11 instead of F5: the program stops at its first statement, and from there F10 walks it line by line.

Stepping into Functions and the Call Stack

  1. In the Solution Explorer, right-click the folder at the top, choose Add > New File…, name it rectangle.scaly, and type the area calculation with a function in it; save it with Ctrl+S:

    function area(w: int, h: int) returns int
    {
        var result w * h
        result
    }
    
    var width 8
    var height 5
    var a area(width, height)
    if a = 40
        print "the area is 40"
    else
        print "unexpected area"
  2. Right-click it and choose Build, then right-click rectangle.exe and choose Set as Startup Item.

  3. Open rectangle.scaly, set a breakpoint on var a area(width, height) and press F5.

  4. Press F11 (Step Into): the editor jumps into the body of area and stops on var result w * h.

  5. The Call Stack window (Debug > Windows > Call Stack, or Ctrl+Alt+C) lists rectangle.exe!area on top and rectangle.exe!main below it. Double-click main: Locals shows width and height. Double-click area: it shows w, h and result.

  6. Press Shift+F11 (Step Out) to return to the caller, then F5 to run to the end.

Arguments and the Environment

  1. In the Solution Explorer, right-click the folder at the top, choose Add > New File…, name it greet.scaly, type this program and save it with Ctrl+S. It greets everyone named on its command line, with the greeting the environment variable GREETING holds, and Hello where there is none:

    var greeting os.environment_variable("GREETING")
    if greeting.length = 0
        greeting := "Hello"
    for name in os.arguments()
        print "`greeting`, `name`!"

    os.arguments() answers the arguments the program was started with, its own name left out; os.environment_variable reads a variable of the environment, and is empty where it is not set.

  2. Right-click greet.scaly and choose Build.

  3. Right-click greet.exe, choose Add Debug Configuration, and in the list that opens, Default. Visual Studio writes .vs\launch.vs.json and opens it.

  4. Add the arguments, the folder as working directory and a variable in the environment to its entry, and save it:

    {
        "version": "0.2.1",
        "defaults": {},
        "configurations": [
            {
                "type": "default",
                "project": "greet.exe",
                "name": "greet.exe",
                "args": [ "Ada", "Grace" ],
                "currentDir": "${workspaceRoot}",
                "env": { "GREETING": "Hello" }
            }
        ]
    }
  5. Right-click greet.exe and choose Set as Startup Item. Set a breakpoint on the print line of greet.scaly and press F5. The program stops there; Locals shows name as "Ada".

  6. Press F5: the next round stops with "Grace". Press F5 once more to run to the end; the console window shows Hello, Ada! and Hello, Grace!.

What Visual Studio Shows

  • The Locals and Autos windows show a function's arguments and its bindings: numbers and booleans; structures, unions and pointers with their fields to expand; a String as its text; a Vector, Array or List with its elements. A generic type is written as in C++, Vector<int>.

  • The Watch and Immediate windows evaluate C++, not Scaly.

  • The editor shows a .scaly file as plain text: no colours, no squiggles, no completion.

A Tour of Scaly

This section sweeps quickly across the language using small, complete programs. If something is not fully explained here, do not worry — every feature is covered in detail in the rest of this specification, and each part of the tour points you to the relevant chapter. Each program prints what it computes; print writes a line:

print "Hello from Scaly"

The same tour runs in the browser, step by step: scaly.io/play/tour.html. There every program of this section stands in an editor with a Run button — change it, run it again, try what happens when you break it; Reset brings the original back. Nothing to install: the compiler runs in the browser tab.

Values and Bindings

Bind a value to a name with let. There is no equals sign — the name is followed directly by its value. Use var for a binding you intend to change, and := to change it. A semicolon starts a comment, and backticks inside a string put a value into the text.

let answer 42        ; immutable binding
var count 0          ; mutable binding
count := count + 1   ; assignment is :=
print "answer is `answer`, count is `count`"

The equals sign = is reserved for comparison, so it never gets confused with assignment. See Lexical Structure for the full lexical rules.

Operators and Operations

Operators are ordinary identifiers, not keywords baked into the grammar. An operation is read from left to right; when operators are chained, they combine by the binding strength the standard library gives them — multiplication binds tighter than addition, comparison weaker than both:

; 3 + 4 * 5 is 3 + 20, which is 23 — not 35
let result 3 + 4 * 5
if result = 23
    print "got 23"
else
    print "unexpected"

Parentheses group explicitly, and an operator written where a value is expected acts as a prefix (-x, not done, a - -2). A call takes its arguments before the operators around it combine, and a call without parentheses takes exactly one argument: print "hi" is print("hi"), but an argument that is itself a call takes its parentheses, print(describe(x)). See Expressions for the complete model, tuples and functions as values included.

Control Flow

if chooses between branches. Single-statement branches need no braces; the condition needs no parentheses:

let n 7
if n < 0
    print "negative"
else if n = 0
    print "zero"
else
    print "positive"

while repeats as long as its condition holds. Use braces for a multi-statement body:

var sum 0
var i 1
while i <= 10
{
    sum := sum + i
    i := i + 1
}
print "the sum is `sum`"

for iterates. for i in 4 runs i from 0 up to — but not including — 4:

var total 0
for i in 4
    total := total + i
print "0 + 1 + 2 + 3 is `total`"

Functions

A function declares its parameter types and return type. The value of the last expression is returned automatically — no return keyword is needed for the final result. A single-expression body needs no braces:

function add(a: int, b: int) returns int
    a + b

print "3 + 4 is `add(3, 4)`"

Functions can call themselves. Here is the classic factorial:

function factorial(n: int) returns int
    if n <= 1
        1
    else
        n * factorial(n - 1)

print "5! is `factorial(5)`"

Functions are pure: they read their arguments and return a value, with no side effects. To mutate, use a procedure.

Procedures and Mutation

A procedure is like a function but may change its inputs and perform side effects. It says which inputs it changes: a parameter it writes is marked mutable, the receiver this included, and a field that may change after the object is built is declared var. A reader always knows which calls can mutate state, and the compiler checks it. See Mutation via Procedures for the details.

define Counter(var value: int)
{
    procedure increment(mutable this)
        value := value + 1

    function get(this) returns int
        value
}

var c Counter(0)
c.increment()
c.increment()
print "the counter is at `c.get()`"

Structures and Methods

Define a structure by listing its fields. A structure with public fields gets a constructor that takes them in order. Methods are functions or procedures whose first parameter is this:

define Point(x: int, y: int)
{
    function distance_squared(this) returns int
        this.x * this.x + this.y * this.y
}

let p Point(3, 4)
print "the distance squared is `p.distance_squared()`"

Constructors covers constructors, including explicit init forms with their own signatures.

Unions, Options, and choose

A union holds one of several variants. Each variant may carry a payload. The most common union is Option, which represents a value that may be absent:

define Option union(Some: int, None)

let present Option.Some(42)
let absent  Option.None

Match on a union with choose. Each when clause names a binding, then the variant, then the result; the binding is bound to the variant's payload. An else clause handles the remaining variants:

define Option union(Some: int, None)

function or_zero(o: Option) returns int
    choose o
        when v: Some
            v
        else
            0

print "`or_zero(Option.Some(7))` and `or_zero(Option.None)`"

See Union Types for union layout, the T? shorthand, and pattern matching in full.

Modules and Imports

Split a program across files with module: the statement module shapes loads the sibling file shapes.scaly. A type defined there stays hidden until the using file imports it with use:

; shapes.scaly
define Box(width: int, height: int)
{
    function area(this) returns int
        width * height
}

; myprog.scaly
module shapes
use myprog.shapes.Box        ; or: use myprog.shapes.*

let b Box(3, 4)
if b.area() = 12
    print "area is 12"

Forgetting the import is caught at compile time, and the error states the exact use line to add. The standard library needs no imports — it is always visible.

Code that several programs share is a package, and a package somebody else wrote is one line away:

package json 0.1.3 "github.com/someone/json"

scaly fetches it from that git repository the first time the program is run, tested or built; and scaly install builds the programs such a package offers and puts them on your PATH. See Modules and Packages for packages and their sources, visibility rules, and glob imports.

Putting It Together

Let us combine the pieces into one small, complete program: a safe division that returns an Option instead of crashing on a zero divisor, with the caller deciding what to print.

Start with the function. It returns None when the divisor is zero, and Some with the quotient otherwise:

define Option union(Some: int, None)

function safe_divide(a: int, b: int) returns Option
    if b = 0
        Option.None
    else
        Option.Some(a / b)

Now call it and react to the result with choose. The Some arm receives the quotient; the else arm handles division by zero:

let result safe_divide(20, 4)
choose result
    when q: Some
        if q = 5
            print "20 / 4 = 5"
        else
            print "unexpected quotient"
    else
        print "cannot divide by zero"

Put both pieces in divide.scaly, then build and run it:

scaly build divide.scaly -o divide
./divide

It prints:

20 / 4 = 5

Change the call to safe_divide(7, 0) and the same program prints cannot divide by zero — the union makes the failure a value you must handle, not a crash you can forget.

From here, the rest of this specification fills in the details: the Lexical Structure rules, the full Expressions model, and the Type System system including generics and the inferred region-based memory management.