amber Documentation

Amber the programming language compiled to Bash

4,696 stars Rust Rust 1 file · ~12,751 tokens #bash#bash-scripting#compilers#hacktoberfest
RAW Doc

1. Project Overview & Quickstart (amber-lang/amber)

Amber

Programming language that compiles to Bash. It's a high level programming language that makes it easy to create shell scripts. It's particularly well suited for cloud services.

[!Warning]
This software is not ready for extended usage.

Join our Discord or
#amber-lang on irc.oftc.net (irc://irc.oftc.net/#amber-lang) or
Join our Matrix room

Install

You can find an installation guide on the documentation website.

Contributing

Contributing guide can be found on the documentation website.

2. Official Technical Reference & Guides (amber-lang/amber-docs)

File: README.md

Official documentation of Amber

What is Amber?

Amber is a cutting-edge programming language compiled into Bash Script. With a focus on modern syntax, safety features, type safety, and practical functionalities that exceed the capabilities of Bash, Amber aims to provide a streamlined and robust scripting experience. The implementation is written in Rust and hosted on GitHub.

Documentation Purpose

This documentation serves as a comprehensive guide to understanding and utilizing the features of Amber. Whether you are a seasoned developer or a newcomer, this documentation will walk you through the unique aspects and advantages that Amber brings to the world of scripting.

Explore the sections here to delve into Amber's syntax, safety measures, and practical functionalities, and discover how it can enhance your scripting workflow. If you have any questions or need assistance, feel free to reach out for support.

Happy scripting with Amber!

Installation

sh
# Install dependencies
npm install
# Generate the stdlib documentation
./sync-stdlib-doc.ab
# Start the development server
npm run dev

File: docs/nightly-alpha/getting_started/faq.md

FAQ

Who is Amber for?

Amber is aimed at developers who need the power of a modern programming language while staying within the ubiquitous Bash environment. It is especially useful for:

  • System administrators automating complex tasks.
  • DevOps engineers writing portable scripts.
  • Developers who want type‑safety and richer abstractions without abandoning the shell.

Why not Python?

Python excels at many domains, but it requires a separate runtime and often incurs additional deployment overhead. Amber compiles directly to Bash, so you can:

  • Leverage existing shell tooling and pipelines.
  • Deploy a single script without installing a language interpreter.
  • Benefit from Bash‑native features (process substitution, job control) while enjoying high‑level syntax and safety.

Why not write Bash directly?

Pure Bash scripts lack static type checking, modular imports, and modern language constructs. Amber provides:

  • A type system that catches errors early.
  • Structured imports and modules for better code organization.
  • Built‑in helpers (e.g., safe string interpolation, automatic error handling) that reduce boilerplate.

How does Amber compare to other languages?

Feature Amber Bash Python
Compiles to Bash ✅ ✅ ❌
Static typing ✅ ❌ ✅
Native shell integration ✅ ✅ ❌
Learning curve Low Moderate Moderate

Amber fills a niche between lightweight shell scripting and full‑featured high‑level languages.

Is Amber production‑ready?

Amber is under active development. The nightly provides the latest features and improvements, but may contain breaking changes.
For production workloads, we recommend using the latest stable release (e.g., 0.5.x) rather than the nightly build.

In the the project it is production ready because it is already used in this context because the Bash code generated is tested and confirmed that works, the language is evolving with the tooling set.

Can I use Amber for system scripts?

Yes. Amber is designed for exactly that purpose. It can replace many typical Bash one‑liners and larger maintenance scripts, offering better readability, safety, and testability while still running on any POSIX‑compatible system.

What shells does Amber support?

Currently, Amber targets Bash versions 3.2 through 5.3. We actively test across:

  • Linux (GNU bash 3.2-5.3)
  • macOS (bash 3.2)

The compiled code is highly portable and will run on any system with the target shell installed.

Can Amber compile to other shells like Zsh or POSIX sh?

Yes, this is actively being developed. Amber currently targets Bash as its primary shell, but we're working on mechanisms to allow targeting different shells. For now, you can use Amber's built-in features that have equivalents in POSIX shell, and we're working on a target configuration that will let you specify the target shell.

Why not just use an LLM to write scripts?

LLMs can generate scripts, but they come with their own challenges:

  • Scripts may only work on your machine with your specific Bash version
  • No type safety or compile-time error checking
  • Security concerns with AI-generated code

Amber provides the ease of writing with a modern syntax while ensuring your scripts work reliably across different environments.

Is Amber better than Python for shell tasks?

It depends on your needs:

Consider Amber if... Consider Python if...
You need maximum portability (only Bash required) You need external dependencies (pip packages)
Your scripts should run on any Unix-like system You control the strict deployment environment
You want type safety in shell scripting You need complex data structures or pip dependencies
You prefer Bash-native features You need complex APIs or specific integration with other technologies

Many users use Amber for system administration and DevOps tasks where portability matters, and Python for application development where ecosystem matters more.

Can I use Amber for CI/CD pipelines?

Absolutely! Amber shines in CI/CD because:

  • Only Bash is required (pre-installed on all CI runners)
  • Type safety catches errors before they reach production
  • Scripts compile to clean, readable Bash
  • Bshchk validation ensures dependencies exist
  • The compiler itself can be installed via various methods (bin, brew, pip, etc.)

We even use Amber to generate parts of our own documentation!

How does Amber handle error cases?

Amber enforces explicit error handling at compile time. If a function can fail, Amber will not compile your code unless you handle both success and failure cases, either by displaying an error message or failing silently. This prevents the "silent failure" problem common in Bash scripts where commands fail but execution continues as if nothing happened.

Can Amber generate Bash that ShellCheck would accept?

Yes! Our focus on clean Bash generation means the compiled code follows ShellCheck (not yet at 100%) best practices. We're also working on integrating ShellCheck into our CI pipeline to ensure the generated code maintain high quality standards.

Is Amber suitable for beginners?

Yes, Amber is designed to be approachable:

  • Syntax similar to JavaScript/Python/Rust (familiar to most developers)
  • Type system catches common mistakes early
  • Built-in functions reduce boilerplate
  • Documentation includes many examples
  • The compiler provides helpful error messages

Many users start with Amber to learn scripting concepts before moving to more complex languages.

Why was Amber created?

The project started as a PhD research topic, focusing on making Bash scripting safer and more maintainable. It gained community interest and evolved into a full project.


File: docs/nightly-alpha/getting_started/getting_started.md

Welcome to the documentation of Amber the programming language! 🎉

Here is a minimal hello world example:

ab
echo("Hello world!")

What is Amber?

Amber is a programming language compiled into Bash Script. It was designed with a modern syntax, safety features, type safety and practical functionalities that Bash could not offer. The subsequent section will demonstrate how Amber embodies these characteristics.

Modern Syntax

Amber is designed based on the ECMA script syntax. The goal was to create a syntax that any developer could feel comfortable with. Hence, Amber draws on features from languages like Rust and Python.

Safety Features

When Bash command fails - it carries on with the code execution as if nothing has happened. This could lead to some serious problems and side effects that are irreversible.

We dislike this behavior. This is why Amber will not compile if edge cases aren't handled - whether that involves displaying an error message to the user or failing silently.

Type Safety

Amber comes with a straightforward type system that aids in identifying simple bugs and errors at compile time, yet it remains unobtrusive, allowing you to focus on what matters most in scripting: the logic.

Extra Features

Amber supports things that are essential to developer like floating point arithmetic, a non-obscure way to handle arrays or even passing variables by reference instead of by copy. In addition to that Amber comes with a standard library that includes features like text trimming, summing all number in an array, splitting text and many more.

Supported Environments

Amber's compiled Bash scripts are actively tested across a range of environments.

Environment Version Range Status Notes
Bash (Linux) GNU 3.2 - 5.3 Under Testing All versions within this range are tested using tianon/docker-bash.
Bash (macOS) 3.2 Under Testing Verified through GitHub Actions macos-latest environments.
Bash (Linux) Busy Box Latest Under Testing Busy box environment. As of right now latest is 5.3

If you're wondering who Amber is for or why to use it instead of other languages, check the FAQ.


File: docs/nightly-alpha/getting_started/installation.md

Installation

Support for architectures

The Amber compiler currently works on:

  • Linux x86 and ARM
  • macOS x86 and ARM (Apple Silicon)
  • Windows over WSL 2

Preparation for installation

macOS

On macOS, you should have everything preinstalled (curl, bash, bc).

Linux

Make sure that the operating system meets the following prerequisites

  • Install the basic calculator:

    • On Debian and Ubuntu: sudo apt install bc
    • On Arch: sudo pacman -Syu bc
    • On Fedora: sudo dnf install bc
    • On OpenSUSE: sudo zypper install bc
  • curl and bash are both installed by default in most cases.
    In the very rare case that they happen to be not available yet, download them as well.

DETAILS: You should always update the system before you install a package in a rolling release distro, such as Arch and Tumbleweed. Always reboot after an update of the kernel, init system, and similar software as well.

Installation with Homebrew

Running this snippet will install latest stable version.

sh
brew install amber-lang/amber/amber-lang

Installation via `bin`, the binary package manager

Install bin itself:

Download the binary for your platform here:

text
https://github.com/marcosnils/bin/releases

And then make it executable:

text
chmod +x ./bin_0.24.2_linux_amd64

And now run it from the directory where it is located:

text
./bin_0.24.2_linux_amd64 install github.com/marcosnils/bin

And now, install Amber:

text
bin install github.com/amber-lang/amber

Update it via:

text
bin update

DETAILS: Bin can install all binaries that are hosted somewhere on GitHub, Codeberg, and other locations. For detailed documentation, see: Commands Reference

Installation via script

  • System-wide
bash
bash <(curl -sL "https://github.com/amber-lang/amber/releases/download/0.5.1-alpha/install.sh")
  • Local-user
bash
bash -- <(curl -sL "https://github.com/amber-lang/amber/releases/download/0.5.1-alpha/install.sh") --user
  • Available versions with package managers

NixOS Channel

The name of the package is amber-lang.

  • configuration.nix
nix
environment.systemPackages = [
    pkgs.amber-lang
  ];
  • And with home manager:
nix
home.packages = with pkgs; [
    amber-lang
  ];
  programs.home-manager.enable = true;
  • Start a shell with:
nix
nix-shell -p amber-lang

NixOS with Flakes

  • You can use the Amber flake like this:
nix
{
    inputs = {
        # ...
        amber.url = "github:amber-lang/Amber";
    };
}
  • Flakes with home manager:
nix
home.packages = [ inputs.amber.packages.${pkgs.system}.default ];

While developing with Nix, the flake defines all dependencies for nix develop (or direnv if used).

Snap

bash
sudo snap install amber-bash --classic

Windows Support

As Windows does not come with bash installed, it makes no sense to support it.
Please install WSL 2 on your Windows machine and install the Linux version of the Amber compiler inside.

For it to work, you may need to run the following code that pulls all the prerequisites.
These count for Debian and Ubuntu-based images.

sh
sudo apt install curl bc
sudo mkdir /opt /usr/local/bin

Integration of external tools

Amber is currently an alpha-stage project, and to implement some features, we have chosen to integrate external tools.
If these tools are available on your system, they will be executed at the end of the Bash compilation process.

  • bshchk: A runtime Bash dependency checker. Install it separately on your system to enable automatic checking of external command dependencies in your compiled scripts. See the bshchk repository for installation instructions.

[!TIP]
bshchk is not included with Amber. For more information about using it, including inline directives and disabling it when needed, see the Postprocessors section in the Usage guide.

Uninstallation

If you have installed it via the script installation option, simply run the following code snippet.

sh
bash -- <(curl -sL "https://github.com/amber-lang/amber/releases/download/0.5.1-alpha/uninstall.sh")

File: docs/nightly-alpha/getting_started/migration_guide.md

This guide provides a step-by-step walkthrough for migrating code from 0.4.0-alpha to 0.5.0-alpha. The current version introduces several breaking changes. This document outlines the modifications, explains how to adapt your code to maintain the same behavior, and highlights updated features. In this guide we will cover two main categories of changes:

  1. Language Features: Changes and updates to the core language syntax and semantics.
  2. Standard Library Updates: Modifications to existing standard library functions and their usage.

Follow along to ensure a smooth transition to the new version. Let’s get started!

New integer `Int` data type

Previously, Amber supported only the Num type. This release introduces Int, which maps to Bash’s native integer arithmetic. To support this, we’ve updated parts of the language syntax.

Array subscript

Expression in the subscript can only be of type Int.

ab
// Before
arr[12.0] // Ok; although fails

// After
arr[12.0] // Error: array subscript can only be an integer

Range

Expressions in range operator can only be of type Int.

ab
// Before
10.0..15.0 // OK; although fails

// After
10.0..15.0 // Error: range can only be applied on integers

Iterator

Iterator variable in for-loop is now of type Int.

ab
// Before
for i, item in items {} // `i` is a `Num`

// After
for i, item in items {} // `i` is an `Int`

Exit

Exit builtin now accepts only expressions of type Int.

ab
// Before
exit 2.0 // Ok; although fails

// After
exit 2.0 // Error: exit accepts only `Int` type

Status

The status builtin now returns a value of type Int.

ab
// Before
status // Returns `Num` value

// After
status // Returns `Int` value

Len

The len builtin now returns a value of type Int.

ab
// Before
len(text) // Returns `Num` value

// After
len(text) // Returns `Int` value

Text to Bool Casting Warning

Casting Text to any values including Bool and Int, now issues an "absurd cast" warning. While not an error, it indicates a potential logical issue and encourages explicit conversion for clarity.

ab
// Before
echo "true" as Bool then 1 else 0 // OK

// After
echo("true" as Bool then 1 else 0) // Warning: Absurd cast

Escaping Changes

String Literal Escaping Changes

A bug related to the escaping of $ sequences within string literals has been fixed. Previously, "\$variable" would incorrectly interpolate the value of variable instead of treating $ as a literal character. If your code inadvertently relied on the previous buggy behavior where \$ within a string literal was interpolated, you will now observe the correct behavior where \$ is treated as a literal dollar sign. You may need to adjust your string literals if you intended interpolation in such cases.

ab
// Before
let var = 45
echo "\$var" // Output: 45

// After
let var = 45
echo("\$var") // Output: \$var

Command String Escaping Changes

The internal handling of text within commands has been refactored, leading to a breaking change in how double quotes (") should be escaped within command strings. Previously, " might have been escaped with \" in some contexts, but this is no longer the correct behavior.

Double quotes (") should not be escaped with a backslash (\) when used within command strings. The parser now handles this automatically. If your code contains command strings where double quotes are escaped (e.g., $ echo \"hello\" $), you must remove the backslash.

ab
// Before (will now cause an error or incorrect behavior)
trust $ printf \"Amber\" $ // Incorrect: will now be interpreted as a literal backslash followed by a double quote

// After (correct behavior)
trust $ printf "Amber" $ // Correct: the double quote is handled by the parser

Standard Library Updates

Redesigned `std/date`

The standard library’s Date module has been completely overhauled. We improved how its functions compose, removed obsolete ones, and repurposed others. The complete list of changes is below.

Old Name New Name Description
date_posix date_from_posix Converts textual representation in a default YYYY-MM-DD HH:MM:SS format to unix epoch time
new date_format_posix Converts unix epoch time to a textual representation.
date_add date_add Adds time to passed date.
new date_sub Subtracts time to passed date.
removed date_compare Compares two dates and returns value of a sign function.

Regex Functions Compatibility Changes

To improve cross-platform compatibility, especially with macOS and BusyBox environments, the standard library functions match_regex() and replace_regex() no longer support certain GNU Sed-specific regular expression features. If your existing code relies on these GNU Sed-specific features within match_regex() or replace_regex(), you will need to update your regular expressions to use POSIX-compliant alternatives. For example, instead of \b, you might use [[:<:]] and [[:>:]] for word boundaries, or ensure you are using ERE for alternation (|).

The following regular expression features are no longer supported within match_regex() and replace_regex():

  • \b (word boundary) in both Extended Regular Expressions (ERE) and Basic Regular Expressions (BRE).
  • | (alternation) in Basic Regular Expressions (BRE).

Function Renaming

The standard library function parse_number has been renamed to parse_num to align with the new Int data type and improve clarity. If your code directly calls parse_number, you will need to update these calls to parse_num.

ab
// Before
const num_val = trust parse_number("123.45")

// After
const num_val = trust parse_num("123.45")

Functions Now Failable

Several standard library functions that previously returned a Bool to indicate success or failure have been updated to be failable functions. This change aligns with Amber's failable paradigm, providing a more consistent and robust error handling mechanism. These functions no longer return a Bool. Instead, they will either succeed or fail, triggering the failable mechanism (e.g., propagating failure with ? or being caught by a failed block).

Affected Functions:

  • std/fs::symlink_create
  • std/fs::dir_create
  • std/fs::file_chmod
  • std/fs::file_chown
  • std/net::file_download
ab
// Before
if dir_create("my_directory") {
    echo "Directory created successfully."
} else {
    echo "Failed to create directory."
}

// After
dir_create("my_directory") exited(code) {
    if code == 0:
        echo("Directory created successfully.")
    else:
        echo("Failed to create directory.")
}

Removed `env_const_get` Function

The env_const_get function has been removed from std/env. Use env_var_get instead, which provides the same functionality.

ab
// Before
env_const_get("VAR")

// After
env_var_get("VAR")

File: docs/nightly-alpha/getting_started/testing.md

Test blocks are dedicated scopes for writing tests. They are executed only when running the amber test command and are ignored during normal compilation. Test blocks can be optionally named using a string literal. This improves readability and allows for targetted execution.

Syntax

ab
import { assert } from "std/test"

// Unique named test block
test "can multiply numbers" {
    let result = 10 * 2
    assert(result == 20)
}

// Unnamed test block (only one allowed per file)
test {
    let name = "Amber"
    assert(name + " Lang" == "Amber Lang")
}

CLI Test Filtering

The amber test command is designed to verify the correctness of your code by executing test blocks. By default, it recursively finds and runs all tests in the current directory. You can narrow down which tests to run by providing filter arguments.

Filtering Tests

You can run a specific subset of tests by providing a filter argument. This argument performs a substring match against both the filename and the test name.

bash
# Run all tests located in the current directory containing "variable" in their name or filename
amber test . "variable"

Targeting Specific Files

Instead of running tests from the current directory, you can specify a particular file or directory to scan.

bash
# Run all tests inside main.ab
amber test main.ab

# Run all tests in main.ab that contain "zip" in their name
amber test main.ab "zip"

File: docs/nightly-alpha/getting_started/usage.md

The Amber CLI can be used as a runtime or as a compiler.

Command Line Interface

The Amber CLI syntax uses subcommands, like the Git CLI:

This output is generated from the 0.5.2-alpha version.

text
Usage: amber [OPTIONS] [INPUT] [ARGS]... [COMMAND]

Commands:
  eval        Execute Amber code fragment
  run         Execute Amber script
  check       Check Amber script for errors
  build       Compile Amber script to Bash
  docs        Generate Amber script documentation
  completion  Generate Bash completion script
  test        Run Amber tests
  help        Print this message or the help of the given subcommand(s)

Arguments:
  [INPUT]    Input filename ('-' to read from stdin)
  [ARGS]...  Arguments passed to Amber script

Options:
      --no-proc <NO_PROC>  Disable a postprocessor
                           Available postprocessors: 'bshchk'
                           To select multiple, pass multiple times with different values
                           Argument also supports a wildcard match, like "*" or "b*chk"
  -h, --help               Print help
  -V, --version            Print version

For detailed usage instructions, refer to the Amber usage guide.

Running Amber Code

The following command will simply execute hello.ab as a script file. Amber code will be compiled to Bash and then executed all in one go:

sh
$ amber run hello.ab
Hello world!

Alternatively, if the file contains a shebang line and has the executable bit set, it can be run like this:

ab
#!/usr/bin/env amber
echo("Hello world")
sh
$ ./hello.ab
Hello world

Additionally, command line arguments can be passed to the script:

ab
#!/usr/bin/env amber
main(args) {
    for arg in args {
        echo(arg)
    }
}
sh
$ ./args.ab 1 2 3
1
2
3

Preventing Execution with Bash

If you write an Amber script with a shebang pointing to amber, there is a risk that someone might accidentally execute it with bash instead. To prevent this, you can add a check at the top of your script using the following technique:

ab
// 2> /dev/null; exit 1

// Your Amber code here
echo("Hello world")

This line is valid in both Amber and Bash:

  • In Amber, // starts a comment, so the line is ignored
  • In Bash, // is treated as a comment (ignored), 2> /dev/null suppresses errors, and exit 1 terminates the script with an error code

For more information about running Amber scripts, see Running Amber Code.

If you want to run just a small code snippet, you can do that as well:

sh
$ amber eval '
import * from "std/text"
echo(uppercase("Hello world!"))
'
HELLO WORLD!

Compiling Amber Scripts

There are times when you prefer to just compile Amber code to a script, for example when dealing with cron jobs:

sh
$ amber build input.ab output.sh

You’ll notice that the compiled script is immediately callable; hence, there’s no need to add executable permissions using chmod, for instance. Amber grants the permission automatically.

Furthermore, Amber adds a shebang at the top of the compiled script. This enables you to run the code simply, without any additional commands:

sh
$ ./output.sh

Testing

Amber comes with a built-in test runner. You can define named test blocks in your code and execute them using the amber test command.

sh
$ amber test

For more details on writing and filtering tests, please refer to the Testing guide.

Syntax Highlighting

VS Code as well as Zed now have built-in LSP integration.

![Amber LSP Feature]{"width": "100%"}(/images/lsp-example-light.webp)(/images/lsp-example-dark.webp)

Here is a list of plugins that support syntax highlighting for Amber language.

Icon Name Location
LOGO:hx Helix Editor Native Support
LOGO:kate Kate/KWrite GitHub
LOGO:nova Nova Nova extensions
LOGO:vim Vim Our extension repository
LOGO:vsc VS Code VSC Marketplace or Our extension repository
LOGO:zed Zed Zed extensions or Our extension repository
JetBrains JetBrains extension

Other interesting commands

Postprocessors

Amber supports postprocessors that can optionally run after compilation to enhance your scripts. These tools are not included with Amber but will be executed automatically if they are installed on your system.

bshchk

bshchk is a runtime Bash dependency checker. It analyzes your compiled Bash script to ensure all external commands used are available at runtime, preventing runtime failures due to unavailable dependencies.

Features:

  • Detects missing external commands before script execution
  • Prevents runtime failures due to unavailable dependencies
  • Supports inline directives for fine-grained control

For installation instructions and usage details, please refer to the bshchk README.

Minification

Additionally, the --minify option compresses the generated Bash code to reduce its size:

sh
$ amber build --minify input.ab output.sh

Generating Amber Documentation

The following command extracts comments prefixed with /// (triple slashes) from a single Amber file, and generates a Markdown file for documentation, by default in the docs subdirectory:

sh
$ amber docs stdlib.ab

Generating Bash Completion Scripts

The following command generates a Bash completion script:

sh
$ amber completion
_amber() {
    local i cur prev opts cmd
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
...

This can be sourced in the .bashrc file via command redirection, so that command completion works in all subsequently opened Bash shells. Assuming the amber binary is on the system path:

sh
$ cat ~/.bashrc
...
source <(amber completion)
...

Disabling the Optimizer

The optimizer is still being improved. If you encounter any issues with optimization, you can disable it using an environment variable:

sh
AMBER_NO_OPTIMIZE=1 amber ...

Amber allows you to customize the header and footer of compiled scripts using environment variables:

AMBER_HEADER: Path to a custom header file that replaces the default header. The header can use template variables:

  • {{ version }} - Amber compiler version

AMBER_FOOTER: Path to a custom footer file that appends to the end of the script. The footer can use:

  • {{ version }} - Amber compiler version

Example custom header (custom_header.sh):

bash
#!/usr/bin/env bash
# Custom header for production scripts
# Project: {{ version }}
# Generated on: $(date)

Example custom footer (custom_footer.sh):

bash
# Custom footer
# End of generated script

Usage:

sh
# Using custom header
AMBER_HEADER=./custom_header.sh amber build input.ab output.sh

# Using both header and footer
AMBER_HEADER=./custom_header.sh AMBER_FOOTER=./custom_footer.sh amber build input.ab output.sh

Default header:

bash
#!/usr/bin/env bash
# Written in [Amber](https://amber-lang.com/)
# version: {{ version }}

Note: Custom headers and footers are useful for adding project-specific metadata, license information, or runtime checks to your compiled scripts.


File: docs/nightly-alpha/getting_started/whats_new.md

WARNING: Brief description of new changes TBD when releasing

Union Types

Union types provide a flexible way to define function parameters that can accept values of multiple distinct types.

ab
fun print_value(val: Int | Text | Bool) {
    echo(val)
}

print_value(42)       // Valid
print_value("Amber")  // Valid
print_value(true)     // Valid

Testing suite

Amber now features a built-in testing suite. It allows you to write dedicated test blocks that are only executed when running the amber test command.

We also introduced a new std/test library. More on that in the Standard library improvements section.

ab
test "can multiply numbers" {
    let result = 10 * 2
    // assertions ...
}

You can also name your tests for better readability and filter them by name or filename using CLI arguments. Read more in the Testing Guide.

Improved variable diagnostics

The compiler now surfaces clearer warnings when variables are not used, helping you catch mistakes earlier. It also warns when a variable declared with let is never modified, encouraging the use of const for bindings that never need reassignment.

ab
let unused = 1 // Warning: variable 'unused' is not used
let count = 3  // Warning: variable 'count' is never modified - consider using 'const'
echo(count)

New builtins syntax

All builtins like echo now have new syntax similar to functions:

ab
echo("Hello world")
cd("newdir")
mv("file.ab", "newdir")
exit(1)

Array Type Resolution

Amber now supports type inference for empty arrays []. You can initialize an empty array without specifying its type immediately. The type will be resolved later based on how the array is used, such as in assignments, binary operations, or function calls.

ab
let arr = [] // Type is generic
arr += [1]   // Resolved to [Int]

Array destructing

You can now destruct arrays into separate variables:

ab
let arr = [1,2,3]
let [key1, key2, key3] = arr

echo("{key1} {key2} {key3}") // 1 2 3

Standard library improvements

WARNING: Brief description of new changes TBD when releasing

New `std/test` module

We introduced a new std/test library that provides assert and assert_eq functions to help you write tests.

ab
import { assert, assert_eq } from "std/test"

test "can multiply numbers" {
    let result = 10 * 2
    assert(result == 20)
    assert_eq(result, 20)
}

New `fetch` function for HTTP requests

The fetch function provides a functionality for making HTTP requests. It intelligently utilizes available command-line tools for network operations, with a failover to bash's network sockets. This function supports a comprehensive set of HTTP methods, including GET, POST, PUT, and DELETE.

ab
import { fetch } from "std/http"

let response = trust fetch("https://example.com")
let post_request = trust fetch("https://example.com", "POST", "hello world!", [ "content-type: text/plain" ]) // POST request

File: docs/nightly-alpha/contribute/compiler.md

The Amber compiler follows a standard compilation pipeline to transform Amber source code into Bash scripts.

mermaid
flowchart LR
    Lexer --> Parser
    Parser --> TypeCheck[Type Checker]
    TypeCheck --> Translator
    Translator --> Optimizer
    Optimizer --> Renderer
  1. Lexer: Transforms source code text into a stream of tokens (Keywords, Operators, Identifiers, etc.).
  2. Parser: Consumes tokens to build an Abstract Syntax Tree (AST), representing the code structure.
  3. Type Checker: Traverses the AST to validate types, ensure safety, and infer missing type information.
  4. Translator: Converts the typed AST into an intermediate representation called FragmentKind (Lower IR).
  5. Optimizer: Performs optimizations on the intermediate representation.
  6. Renderer: Converts the final IR into the target Bash string.

Here you will find out how the compiler is structured, how the parser works and how to write new syntax modules. Let's begin!

Lexer

Beforehand the code is transformed into an array of tokens that contain information about:

  • word - the token content
  • pos - the token location in the document (row, column)
  • start - the index in the code string where the token starts
rs
struct Token {
    word: String,
    pos: (usize, usize),
    start: usize
}

Tokens are created with lexical rules that are contained in the src/rules.rs file.

Here is the example of how an array of Amber tokens can look like, where strings represent instances of Token:

js
[Token<"let">, Token<"is_alive">, Token<"=">, Token<"true">, ...]

Parser

Parser takes in Tokens and forms an Abstract Syntax Tree that represents the code written in Amber.

Syntax Module

What transforms tokens into the AST (Abstract Syntax Tree) is a SyntaxModule. It can be a Text literal, echo builtin or Add operator. The SyntaxModule is a trait that implements:

  • parse method that parses the module and determines whether or not the corresponding token string represents this sytax module. If otherwise, then a Failure::Quiet is returned that means that this is not the correct module to parse the tokens. However if this is the correct module but an error is encountered, then Failure::Loud is returned with an error (or warning / info) is returned.
  • new method that instantiates a new SyntaxModule.
  • syntax_name!("<name of this module>") that identifies this syntax module with its name.

The most important method here is parse that is defined with the following signature:

rs
fn parse(&mut self, meta: &mut M) where M: Metadata -> SyntaxResult;

Parsing returns a SyntaxResult that under the hood is represented as Result<(), Failure>. It means that parsing can be finished successfully or it can fail returning a Failure object.

Here is an example SyntaxModule that parses Bool literal:

rs
impl SyntaxModule for Bool {
    syntax_name!("Bool");

    fn new() -> Self {
        Bool {
            value: false
        }
    }

    fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult {
        let value = token_by(meta, |value| ["true", "false"].contains(&value.as_str()))?;
        self.value = value == "true";
        Ok(())
    }
}

Metadata

You can see that in the parse method mentioned above we pass some object called meta. This is a metadata parameter of type ParserMetadata that inherits from Metadata provided by Heraclitus. The structure instance is carried through the parsing process to keep a track of current state. It holds information such as declared variables, functions, boolean parameters telling if current context is within a loop or a function etc. ParserMetadata is represented as:

rs
struct ParserMetadata {
    // Parsing contenxt
    pub context: Context
    // Error / Warning messages
    pub messages: Vec<Message>
    // ...
}

You can find out more about this structure in src/utils/metadata/parser.rs file.

Parsing Flow

The journey starts with parsing the global Block that can be located in src/modules/block.rs file. The Block parses a sequence of statements (Statement located in src/modules/statement/stmt.rs).

Statement

Statement (Statement) is a structure that can represent any SyntaxModule that is of statement type. In other words Statement is a wrapper for syntax modules that represents a statement type such as loop, if condition, variable declaration etc.

rs
struct Statement {
    value: Option<StmtType>
}

Here we can see that the value field accepts StmtType enum that is declared above and represents a syntax module.

rs
enum StmtType {
    Expr(Expr),
    VariableInit(VariableInit),
    VariableSet(VariableSet),
    IfCondition(IfCondition),
    // ...
}

Statement is built of a macro parse_statement! that can be located in src/modules/statement/stmt.rs. The syntax modules provided to the macro are parsed sequentially in the order from top to bottom. This means that the parser will first try to match Import and then FunctionDeclaration. The expression (Expr located in src/modules/expression/expr.rs) is passed as the final parameter to the parse_statement! macro so that it's parsed at the very end.

rs
parse_statement!([
    Import,
    FunctionDeclaration,
    // ...
    Expr
], ...);

This macro iterates through the provided syntax modules and attempts to parse them one by one. If a module successfully parses the code, the loop breaks and the result is returned.

The StatementDispatch derive macro is used on the StmtType enum to automatically generate dispatch methods for traits like TranslateModule, TypeCheckModule, etc. preventing the need to verify matches manually.

Expr

Expression (Expr located in src/modules/expression/expr.rs) represents a syntax that is a value of certain type (also referred to as kind because of the Rust's type keyword). For example 1 + 1 is an addition of type Num.

rs
struct Expr {
    // The value of the expression
    value: Option<ExprType>,
    // The type of the expression
    kind: Type,
    // The position of the expression
    position: Option
}

Analogically to Statement, expression also is a wrapper for syntax modules that are of expression type. Instead of StmtType enum ExprType is declared.

rs
enum ExprType {
    Bool(Bool),
    Number(Number),
    Text(Text),
    Add(Add),
    Sub(Sub),
    // ...
}

Since certain expressions require different approaches to parsing, there is a different macro used here to automate the process. There are a couple of different types of expressions:

  • TernOp - a ternary operator that is parsed from right to left. It's used for conditional ternary operator.
  • BinOp - a binary operator that is parsed from left to right.
  • UnOp - a unary operator that is parsed from left to right, where the symbol expression is on the left side.
  • TypeOp - a binary expression that is represented as expression followed by operator and then a type. Example of such operator is a cast operator: 12 as Bool.
  • Literal - a Literal that doesn't have any directional precedence. Literal is the final group of expression precedence.

The hierarchy of the groups is represented within the parse_expression! macro (defined in src/modules/expression/macros.rs). It returns an Expr` that has been parsed.

begin[details] How exactly does parsing expressions work?

rs
let result = parse_expression!(meta, [
    ternary @ TernOp => [ Ternary ],
    range @ BinOp => [ Range ],
    addition @ BinOp => [ Add, Sub ],
    multiplication @ BinOp => [ Mul, Div, Modulo ],
    types @ TypeOp => [ Is, Cast ],
    unops @ UnOp => [ Neg, Not ],
    literals @ Literal => [ Bool, Number, Text ]
]);

The pattern that the macro follows can be represented as <function_group_name> @ <group_type> => [<syntax_modules>]. The macro is declared as a recurrent relation of functions (groups) that are calling each other (inside of an internal macro parse_expr_group!).

rs
let result = {
    fn _terminal(...) {
        panic!("Please end the recurrence in the group before");
    }

    fn literal(...) {
        parse_expr_group!(... {literal, _terminal} ...);
    }

    // ...

    fn range(...) {
        parse_expr_group!(... {range, addition} ...);
    }

    fn ternary(...) {
        parse_expr_group!(... {ternary, range} ...);
    }

    return ternary(...);
};

The main objective of parse_expr_group! is to implement given function's body with appropriate parsing mechanism. If it's a BinOp that parses from left to right, then first we parse left expression by calling the lower order group, then we parse the operator, and then the right expression. You can read more on how parsing groups works in the macros file.

end[details]

Type Checking

After the parsing phase is completed, the Compiler performs a Type Check that validates types of variables and expressions. This logic is contained within the TypeCheckModule trait located in src/modules/typecheck.rs.

rs
pub trait TypeCheckModule {
    fn typecheck(&mut self, meta: &mut ParserMetadata) -> SyntaxResult;
}

This method is called after parsing but before translation. It allows the module to infer types, check for type mismatches, and mutate the module state (e.g. storing the inferred type) using the ParserMetadata.

Translation

The next step is translating the AST into an Intermediate Representation (Fragments) which is structurally closer to the target language (Bash). This is handled by the TranslateModule trait located in src/translate/module.rs.

rs
pub trait TranslateModule {
    fn translate(&self, meta: &mut TranslateMetadata) -> FragmentKind;
}

This method takes TranslateMetadata and transforms the module into a FragmentKind.

FragmentKind

FragmentKind is a crucial enum that represents a piece of generated code. It serves as an intermediate representation that abstracts over different types of Bash constructs before they are rendered into the final string. It is defined in src/translate/fragments/fragment.rs.

rs
pub enum FragmentKind {
    Raw(RawFragment),
    VarExpr(VarExprFragment),
    VarStmt(VarStmtFragment),
    Block(BlockFragment),
    Interpolable(InterpolableFragment),
    List(ListFragment),
    Subprocess(SubprocessFragment),
    Arithmetic(ArithmeticFragment),
    Comment(CommentFragment),
    Log(LogFragment),
    #[default] Empty
}

Its purpose is to provide structured generation of Bash code. For example:

  • VarExpr handles variable access (like $VAR).
  • Subprocess handles $(...) command substitutions.
  • Block handles sequences of statements (like { ... }).
  • Arithmetic handles (( ... )) arithmetic contexts.

FragmentKind implements FragmentRenderable trait which allows it to be converted to a string. This is the final stage of compilation where Lower IR is rendered into actual Bash code:

rs
fn to_string(self, meta: &mut TranslateMetadata) -> String;

File: docs/nightly-alpha/contribute/contribute.md

Welcome to the Amber Contributing Guidebook! 👋

This guide offers a clear and comprehensive introduction to getting started with contributing to Amber. If you haven’t already, consider joining our Discord community, where you can ask any questions and connect with other contributors.

Contributing Guidelines

Before you dig into Amber, you should know a few things before you contribute.

Any code change is submitted through a PR, which is then approved by at least 2 maintainers. Each pull request should be directed to the staging branch unless there is a specific justification for targeting the master branch.

The way we talk on GitHub is not the same as we would talk in person. When on GitHub, always get straight to the point and be critical.

Personal grudges are forbidden around here, as well as anything offtopic or offensive.

Opening a PR

Before a PR is opened, it usually has an issue about it first, where we discuss how exactly a feature must be implemented, to avoid making a mistake.

It is recommended that you see how features were already implemented. A good example is #130

To create a PR, you should fork the repo, create a branch, do your work in there, and open a PR. It will then be reviewed and pushed into master.

The maintainers will check who it is the best reviewer, we suggest to open a ticket reporting the issue before starting to do the PR so we can discuss the implementation.

Getting Help

Along the way, you may need help with your code. The best way to ask is in our Discord server, but you may also ask other contributors personally or post in Discussions.

Development

Compile Amber with the following instructions:

text
git clone https://github.com/amber-lang/amber
cd amber
cargo build

In order to execute amber code, use the following command:

bash
# `cargo run` - cargo command
# `run <file.ab>` - amber command
cargo run run <file.ab>

To compile amber code into a bash code, use the following command:

bash
# `cargo run` - cargo command
# `build  <output.sh>` - amber command
cargo run build  <output.sh>

Debugging Amber:

bash
# Displays Amber's AST trace of trying to parse code
AMBER_DEBUG_PARSER=true cargo run -- run <file.ab>
# Shows the time it took to compile each phase
AMBER_DEBUG_TIME=true cargo run -- run <file.ab>
# Flamegraph is a profiling tool that is used to visualize the time each function took to execute
sudo cargo flamegraph -- <file.ab> <file.sh>

Running Tests

Tests modules can be found in src/tests. Modules like erroring.rs, stdlib.rs and validity.rs load test scenarios from directories src/tests/erroring/, src/tests/stdlib/ and src/tests/validity/ respectively.

To run ALL tests, run cargo test.

If you want to run only tests from a specific module, let's say from stdlib.rs, you can do that by adding the module name to the command: cargo test stdlib.

To run a single test case, for example function_with_wrong_typed_return.ab in erroring, you can filter by the test name:
cargo test function_with_wrong_typed_return

Github Actions

We use GitHub Actions to run tests and build binaries. When a new release tag is created, artifacts are built for Linux, macOS, and Windows (x86_64 and aarch64) and uploaded to the release page. We also generate installers and Debian packages.