Biscuit Language (0.10.0)

Compiler usage

Biscuit language compiler is standalone terminal application called blc. It can be compiled from source code found on GitHub repository or downloaded from home page as binary executable. All three major operating systems (Windows, macOS and Linux) are supported, but current active development is done on Windows and it usually takes some time to port latest changes to the other platforms. Compiler executable can be found in bin directory it’s usually good idea to add executable location to system PATH to be accessible from other locations.

There are several options which can be passed to the compiler.

Compiler options:

blc --help
Usage:
  blc [options] [source-files]

Alternative usage:
  blc [options] <-build> [build-arguments]
  blc [options] <-run> <source-file> [arguments] [forwarded-arguments]

Options:
  -build                   Invoke project build pipeline. All following arguments are
                           forwarded into the build script and ignored by compiler itself.
                           Use as '-build [arguments]'.
  -doc                     Generate documentation and exit.
  -opt=<debug|release-fast|release-small>
                           Specify binary optimization mode (use 'debug' by default).
  -release                 Specify binary optimization mode to release. (same as
                           '-opt=release-fast')
  -run                     Execute BL program using interpreter and exit. The compiler
                           expects <source-file> after '-run' flag, the file name and
                           all following command line arguments are passed into the
                           executed program and ignored by compiler itself. Use as
                           '-run <source-file> [arguments]'.
  -shared                  Compile shared library.
  --about                  Print compiler info and exit
  --assert=<default|on|off>
                           Set assert mode ('default' option sets assert 'on' in debug
                           and 'off' in release mode).
  --ast-dump               Print AST.
  --configure              Generate configuration file and exit.
  --di=<dwarf|codeview>    Set debug info format.
  --doc-out-dir=<STRING>   Set documentation output directory. (Use 'out' in current
                           working directory by default.)
  --emit-asm               Write assembly to file.
  --emit-llvm              Write LLVM-IR to file.
  --emit-mir               Write MIR to file.
  --error-limit=<N>        Set maximum reported error count.
  --full-path              Report full file paths.
  --help                   Print usage information and exit.
  --lex-dump               Print tokens.
  --no-analyze             Disable analyze pass, only parse and exit.
  --no-api                 Don't load internal API.
  --no-bin                 Don't write binary to disk.
  --no-color               Disable colored output.
  --no-jobs                Enable single-thread mode.
  --no-llvm                Disable LLVM back-end.
  --no-usage-check         Disable checking of unused symbols.
  --no-warning             Ignore all warnings.
  --reg-split=<off|on>     Enable/disable splitting of structures passed into the
                           function by value into registers.
  --run-tests              Execute all unit tests in compile time.
  --silent                 Disable compiler console logging.
  --syntax-only            Check syntax and exit.
  --time-report            Print compilation time report.
  --verbose                Enable verbose mode.
  --verify-llvm            Verify LLVM IR after generation.
  --vmdbg-attach           Attach compile-time execution debugger.
  --vmdbg-break-on=<N>     Attach compile-time execution debugger and sets break
                           point to the MIR instruction with <N> id.
  --where-is-api           Return path to API folder and exit.

Configuration

Use bl-config tool to change compiler configuration. This tool generates configuration file etc/bl.conf containing all required information needed by compiler during compilation.

Execution status

Compile-time debugger

warning: This feature is not complete, it’s supposed to be used by compiler developers to simplify compiler debugging.

Since the compile-time execution is one of the most powerful things on BL and i.e. command-line utility scripts are executed using the interpreter almost every time, we have to provide a proper way how to debug them. In the case of the compile-time execution, no binary file is produced, so we cannot use external debuggers as gdb or Visual Studio, however, the compiler can be used for debugging directly.

Consider the following example program:

my_function :: fn () {
    ptr: *s32 = null;
    @ptr = 10; // error
}

main :: fn () s32 {
    my_function();
    return 0;
}

The mistake here is obvious, we’re dereferencing the null pointer and the program will just crash during the execution with following error:

$blc -run test.bl

Executing 'main' in compile time...                                             
error: Dereferencing null pointer!                                              
                                                                                
================================================================================
Obtained backtrace:                                                             
================================================================================
test.bl:3:5: Last called:                                                       
   2 |     ptr: *s32 = null;                                                    
>  3 |     @ptr = 10; // error                                                  
   4 | }                                                                        
                                                                                
test.bl:8:5: Called from:                                                       
   7 |     debugbreak;                                                          
>  8 |     my_function();                                                       
   9 |     return 0;                                                            

To track down the error we can use the compiler built-in function debugbreak, causing the execution to be stopped when the interpreter reaches this call.

my_function :: fn () {
    ptr: *s32 = null;
    @ptr = 10; // error
}

main :: fn () s32 {
    debugbreak; // break here
    my_function();
    return 0;
}

When the breakpoint is specified we must execute the program with the compile-time debugger attached to get the actual break. This can be done by the --vmdbg-attach command-line argument.

$blc --vmdbg-attach -run test.bl   
                                    
Executing 'main' in compile time... 
                                    
Hit breakpoint in assembly 'out'.   
                                    
   6 | main :: fn () s32 {          
>  7 |     debugbreak;              
   8 |     my_function();

As you can see, the execution breaks on debugbreak call, and the debugger waits for user input, type h and hit return to get all available commands.

: h                                                                                    
  h, help                             = Show this help.                                
  q, quit                             = Stop debugging.                                
  n, next                             = Step to next instruction.                      
  c, continue                         = Continue execution.                            
  p, print                            = Print current instruction.                     
  bt, backtrace                       = Print current backtrace.                       
  vs=<on|off>, verbose-stack=<on|off> = Log stack operations.                          
  mir=<on|off>, mir-mode=<on|off>     = Enable/disable MIR instruction level debugging.

Now we can step through our code, get some stack-related information, print stack traces on user-code level, but also on MIR instruction level. Printing values of variables is not supported yet.

Build System

Biscuit has an integrated build system replacing CMake or similar tools. Main advantage is integration of the build system directly into the compiler. All you need is build.bl file containing #build_entry function. Setup file is nothing more than a simple BL program executed in compile time with some special features enabled. See Build_System for more information.

Script mode

Programs written in Biscuit can easily act like shell scripts on UNIX systems due to support of shebang character sequence specified at the first line of the entry file. The --silent -run options passed to the compiler reduces all compiler diagnostic output and executes the main function in compile time. No output binary is produced in such a case. Following example can be directly executed in bash as it was executable.

#!/usr/local/bin/blc --silent -run

main :: fn () s32 {
    print("Hello!!!\n");
    return 0;
}
$ chmod +x main.bl
$ ./main.bl

All additional arguments after -run option are automatically forwarded into the executed script and can be accessed via command_line_arguments builtin variable during compile-time execution. First argument (index 0) contains the script name every time.

Automatic Documentation

Integrated self-documentation tool can be used to generate Markdown files from Biscuit source files automatically. Documentation text can be attached to file by //! comment prefix or to declaration by /// comment prefix. Such comments will be recognised by the compiler and attached to proper declaration or file compilation unit. Use -doc compiler flag followed by a list of files you want to generate documentation for. Documentation output will be written to out directory into the current working directory if --doc-out-dir is not specified.

Use marker @INCOMPLETE in documentation comment to mark it as incomplete. Compiler will warn you about symbols with incomplete documentation during generation.

Documentation rules:

Example of documented print function:

/// Write string to the standart output (stdout). Format string can include format specifiers `%`
/// which are replaced by corresponding argument value passed in `args`. Value-string conversion is
/// done automatically, we can pass values of any type as an arguments, even structures or arrays.
///
/// The `print` function accepts C-like escape sequences as `\n`, `\t`, `\r`, etc.
///
/// Pointers to `Error` are dereferenced automatically; so the `print` function can print out errors
/// directly.
///
/// Count of printed bytes is returned.
print :: fn (format: string, args: ...) s32 {
    buf: [PRINT_MAX_LENGTH]u8 #noinit;
    w := _print_impl(buf, format, args);
    __os_write(OS_STDOUT, buf.ptr, auto w);
    return w;
}

Execution of blc -doc print.bl will produce following output:

## print

```
print :: fn (format: string, args: ...) s32
```

Write string to the standart output (stdout). Format string can include format specifiers `%`
which are replaced by corresponding argument value passed in `args`. Value-string conversion is
done automatically, we can pass values of any type as an arguments, even structures or arrays.

The `print` function accepts C-like escape sequences as `\n`, `\t`, `\r`, etc.

Pointers to `Error` are dereferenced automatically; so the `print` function can print out errors
directly.

Count of printed bytes is returned.