Skip to content

Commit

Permalink
updated docs, proper list manipulations and iterators
Browse files Browse the repository at this point in the history
  • Loading branch information
maniospas committed Oct 18, 2024
1 parent a1f2fbb commit a268ca2
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 159 deletions.
Binary file modified blombly.exe
Binary file not shown.
101 changes: 76 additions & 25 deletions docs/advanced/libraries.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Prepacked libraries

Blombly is shipped alongside a couple of small libraries that
you can use out-of-the-box to simplify some aspects of development
and speed up code writting.
Blombly is shipped alongside a few small libraries that
you can use out-of-the-box to simplify some aspects of
development and speed up code writting. These are
detailed here.

## env

This is the most important library that provides an alternative
to some import statements that helps organize versioning and
dependencies. It essentially forms a programmatically-managed
This library provides an alternative to traditional import statements
helps organizes and versions dependencies.
It essentially forms a programmatically-managed
virtual environment that manages libraries.

Under this management system, a library with name `@lib` should
Expand Down Expand Up @@ -43,11 +44,11 @@ the version is in the format `"{version}.{release}"`.
env::include("loop");

A = 1,2;
for(x as loop::next(A))
while(x as loop::next(A))
print(x);

env::versions();
env::help("loop");
env::help("env");
```

```plaintext
Expand All @@ -60,30 +61,80 @@ env 1.0.0
loop 1.0.0
-------------------------------------------------------------
-------------------------------------------------------------
loop 1.0.0
env 1.0.0
Copyright (C) 2024, Emmanouil Krasanakis
Apache 2.0 license
Introduces the `loop::next` macro that allows
iteration over all elements of a list or other
iterable, like so:
A = 1,2,3;
while(x as loop::next(A))
print(x);
This is a wrapper around `std::next` that creates
an iterator for the supplied expression just
before the loop's command.
Introduces version control and documentation
capabilities for other libraries. In particular,
new libraries should contain a library::INFO
code block that sets the following variables:
name, author, license, version, release, year.
Version control substitutes the import statement
and can track imported libraries. Available
macros follow.
- env::include(library);
Includes a library by its name (as a string).
- env::include(library|version=...;minrelease=...);
Includes a library with a specific version and
minimum release number. You may ommit the latter.
- env::help(library);
Prints the details of the library.
- env::versions();
Prints a summary of all libraries introduced
through `env::include` statements.
-------------------------------------------------------------
```

The above example is a variation of the preprocessor
inlcude statement with some simple reporting mixed in.
However, you can also require a version and minimum release
like below. Notice the intentional similarity to method calls,
The above-described inclusion is a variation of the
`#include` preprocessor statement with some simple reporting mixed in.
However, as you can already see from the library's own documentation,
you can also require a version and minimum release like below.
Notice the intentional similarity to method calls,
despite the include statement being a macro.

```java
env::include("loop"|version="1.0";minrelease=0);
```

## loop

This provides a couple of alternatives for constructing
nameless iterators within loop bodies without naming them or
needing additional commands. It achieves this
by employing the `#of` preprocessor statement under the hood.

The first type of iterator is one that is silently constructed
to go through the elements of an iterable. This results to a syntax
like below, which is similar to for/foreach loops of other languages.
At the same time, this syntax introduces minimal new keywords, namely
the `loop::` prefix before usage of `next`.

```java
// main.bb
#include "libs/loop" // or use env::include("loop");
A = 1,2,3;
while(x as loop::next(A))
print(x);
```

A second iterator is for generating ranges of numbers and traversing
through them with (using `next` under the hood). Like so:

```java
// main.bb
#include "libs/loop"
while(i as loop::range(1, 5))
print(i);
```


Both iterators invalidate the simple `next` and `range` keywords,
forcing the developer to choose their behavior with the `std::`
or `loop::` prefixes.
The error message generated in the stead of these keywords explains
the reasoning.
173 changes: 111 additions & 62 deletions docs/advanced/preprocessor.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,53 @@
Blombly's preprocessor can understand several instructions that transform the source code before compilation into `.bbvm` files.
Preprocessor instractions are made distinct by prepending them with a `#` symbol.
Three main types of preprocessing are available: dependencies that make a source code file include another's code,
macros that add to the language's grammar with higher-level expressions, and supporting code transformations.
macros that enrich the language's grammar with higher-level expressions, and other supporting code transformations.

**In practical code writting, you will mostly use dependencies.** The rest of the features are there to support
compile time modification of code, such as by libraries adding new parsable expressions. [Here](libraries.md) are some
libraries that are shipped with the language.
**In practical code writting, you will mostly use dependencies.** Macros and other transformations may alter
the order in which written code is executed. They should be sparingly during development - if at all. Ideally,
they are meant to support certain libraries that inject new coding patterns, like those found [here](libraries.md).
Most new libraries are not expected to use these features either.

## Dependencies
## #include

Dependencies on other `.bb` files or folders are stated with an include statement that looks like this `#include "std/oop"`.
When an include is encountered inlines the contents of the file `std/oop.bb` or, if `std/oop` is a folder of the filter `std/oop/.bb`.
Dependencies allow code modularization without loading overheads, as the compilation outcome packs all necessary instructions to run itself.
Dependencies should not declare specifications, as these are the provenance of code blocks meant to run dynamically instead of immediately upon import.
When creating reusable libraries, prefer constructing modules and final objects (this is automated with the module keyword of `std/oop`).
Dependencies on other `.bb` files or folders are stated with an include statement that looks like so:

```java
#include "libs/loop"
```

## Specifications
When an include statement is encountered inlines the contents of the included path if a suffix `.bb` or `/.bb` is added.
In the above example, this means that either `libs/loop.bb` is included if it exists or, if `libs/loop` is a folder, `libs/oop/.bb` is included.

Specifications have already been described as a mechanism that attaches final values to code blocks. Evaluating them
just after the block's definition.
Dependencies enable code modularization without loading overheads, as the compilation outcome packs all necessary instructions to run
automously by the interpreter. Dependencies should not declare specifications, as these are the provenance of code blocks meant to run
dynamically instead of immediately upon import.
When creating reusable libraries, prefer packing some instructive metadata in some commonly accepted format. One such
format is presented in the [next section](libraries.md).

## Macros

Macros are transformations for reducing boilerplate code. They are declared with statements of the form `#macro (@expression) = (@transformation);`
## #macro

Macros are transformations for reducing boilerplate code. They are declared with statements of the form `#macro {@expression} as {@transformation}`
Both the expression and transformation parts consist of fixed "keyword" tokens and named wildcard tokens, prepended with att (`@`).
Wildcards represent any sequence of tokens and matched between the expression and the transformation.
To support faster compilation and improve comprehension, the first token of the expression needs to be a keyword (e.g., `fn @name (@args)` is valid).
Wildcards represent any sequence of tokens and are matched between the expression and the transformation.

To support faster compilation, improve comprehension, and completely the inherent ambiguity that mixfit operators may create,
the first token of the expression needs to be a keyword (e.g., `fn @name (@args)` is valid). Then, macros are always applied
based on order of occurence.

Here is an example of how macros simplify code, defining a class and function with macros from the std/oop module.
Using these, one can now define classes, modules, and functions more concisely:
Here is an example of how macros alter code writting by defining a simpler version of some found in `libs/oop`:

```java
// oop.bb
#macro (class @name {@code}) = (final @name = {@code});
#macro (fn @name(@args) {@code}) = (final @name(@args) = {@code});
#macro (module @name {@code}) = (final @name = new {@code});
#macro {class @name {@code}} as {final @name = {@code}}
#macro {fn @name(@args) {@code}} as {final @name(@args) = {@code}}
#macro {module @name {@code}} as {final @name = new {@code}}
```

```java
// main.bb
#include "oop.bb"
#include "oop"

class Finder {
fn contains(number) {
Expand All @@ -55,7 +62,7 @@ class Finder {
return true;
}

fn next() {
fn \next() {
while (true) {
this.number = this.number + 1;
if (this.contains(number)) return this.number;
Expand All @@ -65,54 +72,96 @@ class Finder {

finder = new {Finder: number = 10;}

print(finder.next());
print(finder.next());
print(next(finder));
print(next(finder));
```

## Writing an new library
## #of

Below is an example of what a module may look like. Instead of specifications, `final` properties are set for documentation and versioning.
However, methods (which are essentially code blocks meant to be called) obtain specifications.
Notice that those specifications can depend on the library's other properties. To use the module in your code, include the module and call the relevant methods.
The first code transformation we will look at is the `#of` statement.
This is prepended at the beginning of a parenthesis to assign
everything inside to a variable just before the last semicolon `;`
at the same nested level or -if that is not found- at the beginning
of the current code block.

An example and its equivalent
implementation without the preprocessor are presented below. The
difference is that, in the first case, an anonymous variable (starting with
the `_bb` prefix) is internally created instead of `it` to hold the
iterator. That variable replaces the contents of the `#of` parenthesis.

```java
// main.bb
#include "mylib"
A = 1,2,3;
while(x as next(#of iter(A)))
print(x);
```

mylib.tests();
print(mylib.add(1, 2));
```java
// main.bb (equivalent code)
A = 1,2,3;
it = iter(A);
while(x as next(it))
print(x);
```

To properly write loops that traverse all the elements of an iterable,
look at the `loop` library [here](libraries.md).

## #stringify

This converts a sequence of keywords and strings separated by spaces
into one larger string. This operation is executed *at compile time*.
This is especially convenient for converting macro variable names into
strings. Here is an example:

```java
// mylib.bb
#include "libs/oop"
enable oop;

module mylib {
final name = "mylib";
final author = "Your Name";
final doc = "This is a simple numeric interface";
final version = 1.0;

fn add(x, y) {
#spec doc = "Computes an addition in " + name;
#spec version = version;
return x + y;
}
// main.bb
message = #stringify(Hello " world!");
print(message);
```

fn abs(x) {
#spec doc = "Computes the absolute value in " + name;
#spec version = version;
if (x < 0) return -x;
return x;
}
```bash
> blombly main.bb
Hello world!
```

final tests = {
if (add(1, 2) != 3)
fail("Invalid addition");
if (abs(-1) != 1)
fail("Invalid abs");
print("Tests successful.");
}
}
## #symbol

This is similar to `#stringify` with the difference that at the end
it converts the sequence of tokens into a source code symbol. Note that,
by being performed at compile time, this does not (and cannot) take into
account any values associated with the symbol parts. Again, this is
conventient for macro definitions that construct variable names based
on other variable names. Here is an example:

```java
// main.bb
#symbol(var name) = "Hello world!";
print(varname);
```

## #fail

This immediately fails the compilation process upon occurence. This
stringifies the next symbol (or leaves it intact if it is a string)
and immediately exits the compilation process. This is often used
by libraries defining macros with overlap with the standard functionality
to prevent ambiguous symbols from being used. Here is an example:

```java
// main.bb
#macro {next} as {#fail "use std::next instead"} // prevent usage of next
A = 1,2,3;
while(x in next(A))
print(x);
```

```bash
> blombly main.bb
( ERROR ) use std::next instead
→ while(x in # fail "use std::next instead"(A)) main.bb line 3
~~~~~~~~~~~^
→ while(x in # fail "use std::next instead"(A)) main.bb line 3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
```
Loading

0 comments on commit a268ca2

Please sign in to comment.