STD-US-GUIDE(7)

πŸ“¦
version 3.8.0 Β· 3 versions
, in debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– NAME πŸ’–

okay so welcome to std-us-guide bestie β€” it's the AFNIX programmer's guide, aka the Miscellaneous Information Manual entry you're gonna live in. πŸ’…


✨ GETTING STARTED ✨

okay so picture this: AFNIX is a multi-threaded functional engine with dynamic symbol bindings that also fully supports the object oriented paradigm. yes she does both. the runtime engine is state of the art and runs on both 32 and 64 bits platforms. the system spec gives you a rich syntax so functional programming is genuinely a pleasant little activity, no cap.

when you use the interpreter interactively, you type text on the command line and it executes the moment a complete and valid syntactic object has been constructed. or alternatively the interpreter can execute a source file, or it can operate with an input stream. so many options, she's flexible. πŸ¦‹

AFNIX is a comprehensive set of application clients, modules and services. the original distribution has the core interpreter plus extra clients like the:

  • compiler
  • librarian
  • debugger

and it also ships a rich set of modules dedicated to particular domains. the basic modules are:

  • the standard i/o module
  • the system module
  • the networking module

and above modules are services. a service is another extension of the engine that provides extra functionalities with the help of several modules. this hierarchy is strictly enforced in the system design so there's a clear functional separation between application domains. so when you're hunting for a particular feature? always think in terms of module or service functionality. that's the vibe check. ✨

AFNIX operates with a set of keywords and predicates. the engine has a native Unicode database. 🫢 the set of standard objects supports:

  • integers
  • real numbers
  • strings
  • characters
  • boolean

and the containers available in the core distribution are:

  • list
  • vector
  • hash table
  • bitset
  • graphs

the syntax incorporates the concept of a lambda expression with explicit closure. symbol scope limitation within a lambda expression is a feature called a gamma expression. form-like notation with an easy block declaration is another extension compared to other systems. the object model provides a single inheritance mechanism with dynamic symbol resolution. special features include instance parenting, class binding instance inference and deference. native class derivation and method override are also part of the object model, with fixed class objects and forms.

the engine incorporates an original regular expression engine with group matching, exact or partial match and substitution. there's also an advanced exception engine with native run-time compatibility. AFNIX implements a true multi-threaded engine with an automatic object protection mechanism against concurrent access. a read and write locking system that operates with the thread engine is built into the core system. object memory management is automatic inside the core interpreter. πŸ‘‘

and finally β€” the engine is written in C++ and provides runtime compatibility with it. such compatibility includes the ability to:

  • instantiate C++ classes
  • use virtual methods
  • raise or catch exceptions

a comprehensive programming interface has been designed to ease the integration of foreign libraries. slay.


🌈 First contact 🌈

the fundamental syntactic object is a form. a form is parsed and immediately executed by the interpreter. a form is generally constructed with a function name and a set of arguments. the process of executing a form is called the evaluation. the next example shows one of the simplest forms the engine supports. this form just displays the message hello world. iconic behaviour honestly.


πŸŽ€ Hello world πŸŽ€

at the interpreter prompt, a form is constructed with the special object println. the unique argument is a string, and it goes between double quotes.

(axi) println "Hello World"
Hello World

the interpreter can be invoked to enter one or several forms interactively. the form can also live in a text file and the interpreter can be called to execute it. als is the referred extension for a text file, but honestly it can be anything. here's a simple session that executes the above file β€” assuming the original file is called hello.als:

zsh> axi hello.als
Hello World

in interactive mode, the interpreter waits for a form. when a form is successfully constructed, it's immediately executed by the engine. upon completion, the interpreter prompt is displayed and the interpreter is ready to accept a new form. a session is terminated by typing ctrl-d. ✨

another way to use the engine is to call the compiler client called axc, and then invoke the interpreter with the compiled file. the interpreter assumes the .axc extension for compiled files and will automatically figure out which file to execute when a file name is given without an extension. she's smart like that. πŸ’…

zsh> axc hello.als
zsh> axi hello.axc
Hello World
zsh> axi hello
Hello World

the order of search is determined by a special system called the file resolver. its behaviour is described in a special chapter of this manual. iykyk. πŸ“–


πŸ’œ Interpreter command πŸ’œ

the interpreter can be invoked with several options, a file to execute, and some program arguments. the h option prints the various interpreter options.

zsh> axi -h
usage: axi [options] [file] [arguments]
[h]           print this help message
[v]           print system version
[m]           enable the start module
[i   path]    add a resolver path
[e   mode]    force the encoding mode
[f assert]    enable assertion checks
[f nopath]    do not set initial path
[f noseed]    do not seed random engine
[f   seed]    seed random engine

okay so let's break it down:

  • the v option prints the interpreter version and operating system.
  • the f option turns on or off some additional options, like the assertion checking.
  • the use of program arguments is illustrated later in this chapter.
  • the i option adds a path to the interpreter resolver. several i options can be specified, and the order of search is determined by the option order.
  • as mentioned earlier, the use of the resolver combined with the librarian is described in a specific chapter.
  • if the initial file name to execute contains a directory path, such path is added automatically to the interpreter resolver path β€” unless the nopath option is specified.

πŸ¦‹ Interactive line editing πŸ¦‹

line editing capabilities are provided when the interpreter is used interactively. error messages are displayed in red if the terminal supports colors. πŸ“ various accelerators are bound to the terminal, as listed below:

  • backspace β€” erase the previous character
  • delete β€” erase at the cursor position
  • insert β€” toggle insert with in-place
  • ctrl-a β€” move to the beginning of the line
  • ctrl-e β€” move to the end of the line
  • ctrl-u β€” clear the input line
  • ctrl-k β€” clear from the cursor position
  • ctrl-l β€” refresh the line editing

the arrow keys are also bound to their usual functions:

  • left β€” move the cursor to the left
  • right β€” move the cursor to the right
  • up β€” move up in the history list
  • down β€” move down in the history list
note that when using the history, a multi-line command editing access is provided by the interpreter. bestie that's so useful. πŸ’–

🌸 Command line arguments 🌸

the interpreter command line arguments are stored in a vector called argv, which is part of the interp object. a complete discussion about object and class is covered in the class object chapter. for now, it's just necessary to note that a method is invoked by a name separated from the object symbol name with a semicolon. the example below illustrates the use of the vector argument.

# argv.als
# print the argument length and the first one
println "argument length: " (interp:argv:length)
println "first argument : " (interp:argv:get 0)
zsh> axi argv.als hello world
2
hello

🧚 Loading a source file 🧚

the interpreter object also provides the load method to load a file. the argument must be a valid file path, or an exception is raised. the load method returns nil. when the file is loaded, the interpreter input, output and error streams are used. the load operation reads one form after another and executes them sequentially.

# load the source file demo.als
(axi) interp:load "demo.als"
# load the compiled file demo.axc
(axi) interp:load "demo.axc"
# load whatever is found
(axi) interp:load "demo"

the load method operates with the help of the interpreter resolver. by default the source file extension is als. if the file has been compiled, the axc extension can be used instead β€” this forces the interpreter to load the compiled version. if you're not sure, or you don't care which file gets loaded, the extension can be omitted. without an extension, the compiled file is searched first. if it's not found, the source file is searched and loaded. periodt. πŸ’…


⭐ The compiler ⭐

the client axc is the cross compiler. it generates a binary file that can be run across several platforms. the h option prints the compiler options.

usage: axc [options] [files]
[h]            print this help message
[v]            print version information
[i] path       add a path to the resolver
[e  mode]      force the encoding mode

one or several files can be specified on the command line. the source file is searched with the help of the resolver. the resolver i option can be used to add a path to the resolver.


🌈 Writing structure 🌈

the structure of a file is a succession of valid syntactic objects separated by blank lines or comments. during the compilation or execution process, each syntactic object is processed one after another in a single pass. reserved keywords are an integral part of the writing systems. the association of symbols and literal constitutes a form. a form is the basic execution block in the writing system. when the form uses a reserved keyword, it's customary to refer to it as a special form.


πŸ’– Character set and comments πŸ’–

the writing system operates with the standard Unicode character set. comments start with the character #. all characters are consumed until the end of line. comments can be placed anywhere in the source file. comments entered during an interactive session are discarded. πŸ™ˆ


πŸ‘‘ Native objects πŸ‘‘

the writing system operates mostly with objects. an object is created upon request, or automatically by the engine when a native representation is required. to perform this task, several native objects are built inside the engine:

  • Boolean for boolean objects
  • Integer, Relatif for integer numbers
  • Real for floating-point number
  • Complex> for complex number
  • Byte
  • Character and String for character or string manipulation

most of the time, a native object is built implicitly from its lexical representation, but an explicit representation can also be used. πŸ’…


const boolean  true
const integer  1999
const relatif  1234567890R
const complex  1.0+1.0i
const real     2000.0
const string   "afnix"
const char     'a'
trans symbol   "hello world"
trans symbol   2000

okay so πŸ’… the const and trans keywords are the reserved keywords you use to declare a new symbol. And a symbol? bestie, it's literally just a binding between a name and an object. that's it. that's the whole vibe. and the gag is you can use almost any standard character to declare one. iconic.

  • const β€” makes a constant symbol, and returns the last evaluated object. so yes, nested const constructs are totally a thing, like trans b (const a 1). slay.
  • trans β€” declares a new transient symbol. when a symbol is marked transient, the object bound to it can be changed. with a constant symbol? nope, not happening, no cap.

and eventually a symbol can be destroyed with the special form unref. 🫢 but listen β€” it's the symbol that gets destroyed, not the object associated with it. important. write that down.


πŸ›‘βœ¨ Stop and Resume Parsing βœ¨πŸ›‘

the parsing process stops the moment it sees the β—€ character (Unicode U+25C0). and it resumes with the β–Ά character (Unicode U+25B6). πŸ’–

this is lowkey so useful when you're dealing with multi line statements, fr fr. and it's also just a gorgeous little example of Unicode based control characters. we love a glow-up moment. 🌈


πŸŽ€ Forms πŸŽ€

an implicit form is a single line command. simple. but when a command starts getting complicated, the standard form notation is just... more readable, okay? 😌

the standard form uses the ( and ) characters to start and close a form. and a form causes an evaluation. here's the tea on how that goes down:

  1. when a form is evaluated, each symbol in the form gets evaluated to its corresponding internal object.
  2. then the interpreter treats the first object of the form as the object to execute.
  3. and the rest is the argument list for that calling object.

putting a form inside a form is the standard way to do recursive evaluation with complex expressions. nested forms are giving main character energy. πŸ‘‘

const three (+ 1 2)

this example defines a symbol initialized with the integer 3, which is the result of the computation (+ 1 2). and it also shows you that Polish notation is used for arithmetic. + is a built-in operator that makes the arguments get summed (if possible). periodt.

evaluation can be nested, and so can definition and assignation. when a form is evaluated, the result of that evaluation is made available to the calling form. and if the result shows up at the top level? it just gets discarded. πŸ™ˆ

const  b (trans a (+ 1 2))
assert a 3
assert b 3

this program shows you the whole evaluation process in action. the evaluation happens recursively. the (+ 1 2) form evaluates to 3, and that result gets transmitted to the form (trans a 3). that form doesn't just create the symbol a and bind the integer 3 to it β€” it also returns 3, which is the result of the previous evaluation. finally, the form (const b 3) is evaluated: the symbol b is created and the result is discarded. πŸ’…

internally things are a little more complex, but bestie, the idea stays the same. this program also shows you how the assert keyword is used. πŸŽ€


πŸ¦‹ Lambda Expression πŸ¦‹

a lambda expression is just another name for a function. the term comes historically from Lisp, to express the fact that a lambda expression is analogous to the concept of expression found in the lambda calculus. iykyk. 🧚

there are various ways to create a lambda expression. one way is with the trans reserved keyword. a lambda expression takes 0 or more arguments and returns an object. and plot twist β€” a lambda expression is also an object by itself. ✨

when a lambda expression is called, the arguments are evaluated from left to right. then the function is called and the object result is transmitted to the calling form. the whole trans vs const thing gets explained later, hold on.

to illustrate, here's the computation of an integer factorial:

# declare the factorial function
trans fact (n) (
  if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)

this example needs several comments, so let's go:

  • first, the trans keyword defines a new function object with one argument called n.
  • the body of the function is defined with the if special form and is honestly pretty easy to understand.
  • the function gets called in the next form when the println special form is executed.
  • and note that here, the call to fact produces an integer object, which gets converted automatically by the println keyword. automatic? we love to see it. πŸ’–

🌸 Block Form 🌸

the notation used in the fact program is the standard form notation originating from Lisp and the Scheme dialect. but there's another notation called the block form notation, which uses the { and } characters. πŸ’…

a block form is a syntactic notation where each form in the block gets executed sequentially. and each form can be either an implicit or a regular form. so the fact procedure can be rewritten with the block notation, like this:

# declare the factorial procedure
trans fact (n) {
  if (== n 1) 1 (* n (fact (- n 1)))
}
# compute factorial 5
println "factorial 5 = " (fact 5)

another way to create a lambda expression is via the lambda special form. remember, a lambda expression is an object. so when that object gets created, it can be bound to a symbol. so the factorial example could be rewritten with an explicit lambda call:

# declare the factorial procedure
const fact (lambda (n) (
    if (== n 1) 1 (* n (fact (- n 1)))))
# compute factorial 5
println "factorial 5 = " (fact 5)

and note that here, the symbol fact is a constant symbol. the use of const is reserved for the creation of gamma expression. πŸŽ€


πŸ‘‘ Gamma Expression πŸ‘‘

a lambda expression can somehow become very slow during execution, because the symbol evaluation happens inside a set of nested calls to resolve the symbols. in other words, each recursive call to a function creates a new symbol set which is linked with its parent. and when the recursion gets deep? so does the path you have to traverse from the lower set up to the top one. 😭

so there's another mechanism called gamma expression, which binds only the function symbol set to the top level one. the rest stays the same. and using a gamma expression can speed up execution significantly. πŸ”₯

# declare the factorial procedure
const fact (n) (
  if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)

we'll come back to the concept of gamma expression later. and now the use of the reserved keyword const to declare a gamma expression makes total sense, right? since most function definitions are constant with one level, it was a design choice to implement this syntactic sugar. ✨

note that gamma is a reserved keyword and can be used to create a gamma expression object. but on the other hand β€” and this is important, bestie β€” the gamma expression mechanism does not work for instance method. we'll illustrate that point later. πŸ™ˆ


🧚 Lambda Generation 🧚

a lambda expression can be used to generate another lambda expression. in other words, a function can generate a function, and that capability is an essential ingredient of the functional programming paradigm. πŸ’…

the interesting part with lambda expressions is the concept of closed variables. in the next example, look at the lambda expression inside gen: notice that the argument to the gamma is x, while n is marked in a form before the body of the gamma. that notation means the gamma should retain the value of the argument n when the closure is created.

in the literature you might find a similar mechanism referenced as a closure. a closure is simply a variable which is closed under a certain context. and when a variable is referenced in a context without any definition, that variable is called a free variable. we'll see more programs with closures later. and note: it's the object created by the lambda or the gamma call which is called a closure. πŸŽ€

# a gamma which creates a lambda
const gen (n) (
  lambda (x) (n) (+ x n))
# create a function which add 2 to its argument
const add-2 (gen 2)
# call add-2 with an argument and check
println "result = " (add-2 3)

in short:

  • a lambda expression is a function with or without closed variables, which works with nested symbol sets, also called namesets.
  • a gamma expression is a function with or without closed variable which is bounded to the top level nameset.
  • the reserved keyword trans binds a lambda expression.
  • the reserved keyword const binds a gamma expression.
  • and a gamma expression cannot be used as an instance method. periodt.

🫢 Multiple Arguments Binding 🫢

a lambda or gamma expression can be defined to work with extra arguments using the special args binding. during a lambda or gamma expression execution, the special symbol args is defined with the extra arguments passed at the call. for example, a gamma expression with 0 formal argument and 2 actual arguments has args defined as a cons cell. slay.

const proc-nilp (args) {
  trans result 0
  for (i) (args) (result:+= i)
  eval result
}
assert 3 (proc-nilp 1 2)
assert 7 (proc-nilp 1 2 4)

the symbol args can also be defined with formal arguments. in that case, args is defined as a cons cell with the remaining actual arguments. πŸ’–

# check with arguments
const proc-args (a b args) {
  trans result (+ a b)
  for (i) (args) (result:+= i)
  eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)
🚨 okay bestie, this one matters: it is an error to specify formal arguments after args. and multiple args formal definitions are not allowed. no exceptions, fr fr.

the symbol args can also be defined as a constant argument. 🌸

# check with arguments
const proc-args (a b (const args)) {
  trans result (+ a b)
  for (i) (args) (result:+= i)
  eval result
}
assert 7 (proc-args 1 2 4)

✨ Nameset and bindings ✨

okay so a nameset is literally just a container of bindings between a name and a symbolic variable. and what's a symbolic variable, you ask? bestie it's any binding between a name and an object, that's it, that's the whole thing 🫢

there's a bunch of ways to express these bindings. the most common one is called a symbol. another type of binding is an argument. even though they're different, they share a lil set of common properties β€” like being settable, periodt.

another thing to clock: the nature of the nameset itself. fr there are various types:

  • the top level nameset is called a global set, and it's built to handle a large number of symbols πŸ‘‘
  • inside a lambda or gamma expression, the nameset is called a local set, and it's designed to be fast with a small number of symbols ⚑

the moral of this lil story? always think in terms of namesets, no matter how it's implemented. the implementation is not the vibe, the nameset is the vibe.

all namesets support the concept of a parent binding. when a nameset gets created (typically during the execution of a lambda expression), it gets linked with its parent one. which means a symbol look-up happens by traversing all namesets from the bottom to the top, stopping the moment one is found πŸ”

and in terms of notation, here's the cheat sheet:

  • the current nameset is referenced with the special symbol .
  • the parent nameset is referenced with the special symbol ..
  • the top level nameset is referenced with the symbol ...

🌸 Symbol 🌸

a symbol is an object that defines a binding between a name and an object. when a symbol gets evaluated, the evaluation process literally consists in returning the associated object. simple as that.

there are various ways to create or set a symbol, and the different reserved keywords account for the various natures of binding that need to happen depending on the current nameset state.

one of a symbol's properties is whether it's const or not. when a symbol is marked as a constant, it cannot be modified. and note this bestie β€” it's the symbol which is constant, not the object. huge difference, don't mix them up πŸ™ˆ

a symbol can be created with the reserved keywords const or trans.


πŸŽ€ Creating a nameset πŸŽ€

a nameset is an object which can be constructed directly by using the object construction notation. once the object is created, it can be bounded to a symbol. here's a nameset called example in the top level nameset:

# create a new nameset called example
const example (nameset .)
# bind a symbol in this nameset
const example:hello "hello"
println example:hello

πŸ¦‹ Qualified name πŸ¦‹

in the previous example, a symbol is referenced in a given nameset by using a qualified name like example:hello. a qualified name defines a path to access a symbol. using a qualified name is a powerful notation to reference an object in reference to another object, no cap πŸ’…

for example:

  • the qualified name .:hello refers to the symbol hello in the current nameset
  • the qualified name ...:hello refers to the symbol hello in the top level nameset

and there are other uses for qualified names too, like method call with an instance. iykyk.


πŸ’– Symbol binding πŸ’–

the trans reserved keyword has been shown in all the previous examples. the reserved keyword trans creates or sets a symbol in the current nameset.

so for example, the form trans a 1 is evaluated as follows. first, a symbol named a is searched in the current nameset. at this stage, two situations can occur:

  1. if the symbol is found, it is set with the corresponding value.
  2. if the symbol is not found, it is created in the current nameset and set.

the use of a qualified name is also permitted β€” and encouraged β€” with trans πŸ‘‘ the exact nature of the symbol binding with a qualified name depends on the partial evaluation of the qualified name.

for example, trans example:hello 1 will set or create a symbol binding in reference to the example object:

  • if example refers to a nameset, the symbol is bound in this nameset
  • if example is a class, hello is bounded as a class symbol

in theory, there is no restriction to use trans on any object. but if the object does not have a symbol binding capability, an exception is raised. for example, if n is an integer object, the form trans n:i 1 will fail πŸ’€

with 3 or 4 arguments, trans defines automatically a lambda expression. this notation is a syntactic sugar ✨ the lambda expression is constructed from the argument list and bounded to the specified symbol. the rules used to set or define the symbol are the same as described above.

# create automatically a lambda expression
trans min (x y) (if (< x y) x y)

⭐ Constant binding ⭐

the const reserved keyword is similar to trans, except that it creates a constant symbol. once the symbol is created, it cannot be changed. this constant property is held by the symbol itself. when trying to set a constant symbol, an exception is raised 🚨

the reserved keyword const works also with qualified names, and the rules described previously are the same. when a partial evaluation is done, the partial object is called to perform a constant binding. if such capability does not exist, an exception is raised.

with 3 or 4 arguments, const defines automatically a gamma expression. like trans, the rules are the same except that the symbol is marked constant.

# create automatically a gamma expression
const max (x y) (if (> x y) x y)

🌈 Symbol unreferencing 🌈

the unref reserved keyword removes a symbol reference in a given context. when the context is a nameset, the object associated with the symbol is detached from the symbol, eventually destroyed with the symbol removed from the nameset 🫑

# create a symbol number
const x 1
# unreference it
unref x

πŸ“ Arguments πŸ“

an expression argument is similar to a symbol, except that it is used only with function arguments. the concept of binding between a name and an object is still the same, but with an argument, the object is not stored as part of the argument β€” it's stored at another location, which is the execution stack.

an argument can also be constant. on the other hand, a single argument can have multiple bindings β€” that situation shows up during the same function call in two different threads πŸ‘€

an argument list is part of the lambda or gamma expression declaration. if the argument is defined as a constant argument, a sub form notation is used to define this matter. for example, the max gamma expression is given below:

# create a gamma expression with const argument
const max (gamma ((const x) (const y)) (if (> x y) x y))

a special symbol named args is defined during a lambda or gamma expression evaluation, with the remaining arguments passed at the time the call is made. the symbol can be either nil or bound to a list of objects.

const proc-args (a b) {
  trans result (+ a b)
  for (i) (args) (result:+= i)
  eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)

πŸ‘‘ Special forms πŸ‘‘

special forms are reserved keywords which are most of the time imperative statements, as part of the writing system. special forms are an integral part of the writing system and interact directly with the interpreter. in most cases, a special form returns the last evaluated object. and most of the special forms are control flow statements, periodt πŸ’…


If special form

the if reserved keyword takes two or three arguments. the first argument is the boolean condition to check:

  • if the condition evaluates to true, the second argument is evaluated, and the form returns the result of that evaluation
  • if the condition evaluates to false, the third argument is evaluated, or nil is returned if it does not exist

an interesting example which combines the if reserved keyword and a deep recursion is the computation of the Fibonacci sequence:

const fibo (gamma (n) (
    if (< n 2) n (+ (fibo (- n 1)) (fibo (- n 2))))

While special form

the while reserved keyword takes 2 or 3 arguments.

  • with 2 arguments, the loop is constructed with a condition and a form
  • with 3 arguments, the first argument is an initial condition that is executed only once

when an argument acts as a loop condition, the condition evaluates to a boolean. the loop body is executed as long as the boolean condition is true πŸ”

an interesting example related to integer arithmetic with a while loop is the computation of the greatest common divisor, aka gcd:

const gcd (u v) {
  while (!= v 0) {
    trans r (u:mod v)
    u:= v
    v:= r
  }
  eval u
}

note in this previous example the use of the symbol =. the qualified name u:= is in fact a method call. here, the integer u is assigned with a value. in this case, the symbol is not changed β€” it's the object which is muted.

in the presence of 3 arguments, the first argument is an initialization condition that is executed only once. in this mode, it is important to note that the loop introduces its own nameset. the loop condition can be used to initialize a local condition variable.

while (trans valid (is:valid-p)) (valid) {
  # do something
  # adjust condition
  valid:= (and (is:valid-p) (something-else))
}

Do special form

the do reserved keyword is similar to the while reserved keyword, except that the loop condition is evaluated after the body execution. the syntax call is opposite to the while. the loop can accept either 2 or 3 arguments.

  • with 2 arguments, the first argument is the loop body and the second argument is the exit loop condition
  • with 3 arguments, the first argument is the initial condition that is executed only once
# count the number of digits in a string
const number-of-digits (s) {
  const len (s:length)
  trans index 0
  trans count 0
  do {
    trans c (s:get index)
    if (c:digit-p) (count:++)
  } (< (index:++) len)
  eval count
}

Loop special form

the loop reserved keyword is another form of loop. it takes four arguments:

  1. the first is the initialize form
  2. the second is the exit condition
  3. the third is the step form
  4. the fourth is the form to execute at each loop step

unlike the while and do loops, the loop special form creates its own nameset, since the initialize condition generally creates new symbols for the loop only 🌸

# a simple loop from 0 to 10
loop (trans i 0) (< i 10) (i:++) (println i)

a loop can also be designed with a Counter object. in this case, a counter is created with an initial and final count values. the counter step-p method can then be used to run the loop:

# a counter from 0 to 10
trans cntr (Counter 10)
# a simple loop from 1 to 10
loop (cntr:step-p) (println cntr)

in this example, the counter prints from 1 to 10, since the counter is designed to operate from 0 to 9, and the println function is called after the step-p predicate πŸ’–

πŸ”€ Switch special form

ok so switch is a reserved keyword and it's literally just a condition selector, a lil matchmaker πŸ’… the first argument is the switch selector, and the second argument is a list of values that could get matched by the switch value. There's also this special symbol called else that matches anything, so it's your safety net, bestie.

# return the primary color in a rgb
const get-primary-color (color value) (
  switch color (
    ("red"   (return (value:substr 0 2)))
    ("green" (return (value:substr 2 4)))
    ("blue"  (return (value:substr 4 6)))
  ))

see how clean that is?? three cases, three returns, zero chaos ✨


🫢 Return special form

return is a reserved keyword and it signals an exceptional condition in the flow of execution inside a lambda or gamma expression. When a return gets executed, the associated argument is returned and the execution just… stops. periodt. And if you use return at the top level? the result is simply discarded. gone. not stored. πŸ™ˆ

# initialize a vector with a value
const vector-init (length value) {
  # treat nil vector first
  if (<= length 0) return (Vector)
  trans result (Vector)
  do (result:add value) (> (length:--) 0)
}

πŸ‘‘ Eval and protect

eval is a reserved keyword that forces the evaluation of the object argument. You'll usually see eval chilling inside a function body when it needs to return a particular symbol value. It can also force the evaluation of a protected object. And lowkey? in many cases eval is more efficient than return πŸ”₯

Then there's protect β€” also a reserved keyword β€” which constructs an object without evaluating it. Typically, when you use it with a form, protect returns the form itself. It can also stop a symbol from being evaluated: when used with a symbol, the symbol object itself is what comes back.

const add (protect (+ 1 2))
(eval add)

and note this bestie β€” in that example above, the evaluation returns a lambda expression which gets evaluated immediately, and that returns the integer 3. it's giving inception but make it math πŸŒ€


βœ… Assert special form

assert is a reserved keyword that checks for equality between the two arguments and aborts the execution if it fails. dramatic? yes. necessary? also yes.

By default, assertion checking is turned off, and it can be activated with the command option f assert. Needless to say, assert is used for debugging purposes.
assert true   (> 2 0)
assert 0      (- 2 2)
assert "true" (String true)

look at her go, all three of those pass πŸ’…


πŸ“¦ Block special form

block is a reserved keyword that executes a form in a new local set. That local set gets destroyed the second the execution finishes β€” rip little nameset πŸͺ¦ β€” and block returns the value of the last evaluated form. Since a whole new local set is created, any new symbol you create in this nameset is destroyed when execution completes. In other words: block lets you make a local scope. iconic behaviour honestly.

trans a 1
block {
  assert    a 1
  trans     a (+ 1 1)
  assert    a 2
  assert ..:a 1
}
assert 1 a

inside the block, a is a whole different girl. outside? untouched. we love encapsulation πŸŽ€


🌈 Built-in objects

Several built-in objects and built-in operators for arithmetic and logical operations are also integrated into the writing system. The Integer and Real classes are primarily used to manipulate numbers. The Boolean class handles boolean operations. Other built-in objects include Character and String. The exact usage of these objects is described in the next chapter β€” so stay tuned, no spoilers πŸ“

βž• Arithmetic operations

Support for arithmetic operations comes with the standard operator notation. Normally these operators tolerate various object type mixing, and the returned value will generally be bound to an object that provides the minimum loss of information. Most of the operations are done with the +, -, * and / operators.

(+ 1 2)
(- 1)
(* 3 5.0)
(/ 4.0 2)

the way she just mixes types and keeps it cute 😭

🧠 Logical operations

The Boolean class represents the boolean values true and false. Those last two symbols are built-in in the interpreter as constant symbols. There are also special forms like not, and and or. Their usage is self understandable, fr fr.

not true
and true (== 1 0)
or (< -1 0) (> 1 0)

πŸ” Predicates

A predicate is a function which returns a boolean object. There's always a built-in predicate associated with a built-in object. By convention, a predicate terminates with the sequence -p. The nil-p predicate is a special one β€” it returns true if the object is nil. And object-p is the negation of nil-p. πŸ’–

  • nil-p β€” check nil object
  • eval-p β€” check evaluation
  • real-p β€” check real object
  • regex-p β€” check regex object
  • object-p β€” check for non nil object
  • string-p β€” check string object
  • number-p β€” check number object
  • method-p β€” check method object
  • boolean-p β€” check boolean object
  • integer-p β€” check integer object
  • character-p β€” check character object

For example, one can write a function which returns true if the argument is a number, that is, an integer or a real number:

# return true if the argument is a number
const number-p (n) (
  or (integer-p n) (real-p n))

Special predicates for functional and symbolic programming are also built-in in the engine:

  • class-p β€” check class object
  • thread-p β€” check thread object
  • promise-p β€” check promise object
  • lexical-p β€” check lexical object
  • literal-p β€” check literal object
  • closure-p β€” check closure object
  • nameset-p β€” check nameset object
  • instance-p β€” check instance object
  • qualified-p β€” check qualified object

Finally, for each object, a predicate is also associated. For example, cons-p is the predicate for the Cons object and vector-p is the predicate for the Vector object. πŸ¦‹

Another issue related to evaluation is deciding whether or not an object can be evaluated. The predicate eval-p β€” which is a special form β€” is designed to answer exactly that question. Furthermore, eval-p is useful to decide whether or not a symbol is defined, or if a qualified name can be evaluated.

assert true  (eval-p .)
assert false (eval-p an-unknown-symbol)

πŸŽ€ Class and instance

Classes and instances are the fundamental objects that provide support for the object oriented paradigm. A class is a nameset which can be bounded automatically when an instance of that class is created. And the class model? sloppy. 😌 Compared to other systems, there's no need to declare the data members for a particular class β€” data members are created during the instance construction. An instance can also be created without any reference to a class. Methods can be bound to the class, or the instance, or both. And an instance can also be muted during the execution process. main character energy fr.

πŸ›οΈ Class and members

A class is declared with the reserved keyword class. The resulting object acts like a nameset, and it's possible to bind symbols to it.

# create a class object
const Circle (class)
const Circle:PI 3.1415926535
# access by qualified name
println Circle:PI

In that example, the symbol Circle is created as a class object. With the help of a qualified name, the symbol PI is created inside the class nameset. In this case, the symbol PI is invariant with respect to the instance object. A form can also be bound to the class nameset. In both cases, the symbol or the form is accessed with the help of a qualified name. πŸ’…

🌸 Instances

An instance of a class is created like any built-in object. And if a method called preset is defined for that class, that method is used to initialize the instance.

# create a class
const Circle (class)
trans Circle:preset (r) {
  const this:radius (r:clone)
}
# create a radius 1 circle
const c (Circle 1)

This example calls for several comments, bestie:

  1. First, the preset lambda expression is bound to the class. Since preset is a reserved name for the class object, the form is automatically executed at the instance construction. automatic. no manual labour 😀
  2. Second, note that the instance data member radius is created by the lambda expression, and another reserved keyword called this is used to reference the instance object β€” as is customary with other programming systems.

🧚 Instance method

When a lambda expression is bound to the class or the instance, that lambda can be invoked as an instance method. When an instance method is invoked, the instance nameset is set as the parent nameset for that lambda. This is the main reason why a gamma expression cannot be used as an instance method. Therefore, the use of the reserved keyword this is not recommended in a gamma expression β€” although it's perfectly acceptable to create a symbol with such a name. iykyk πŸ™ˆ

# create a perimeter method
trans Circle:perimeter nil (
  * (* 2.0 Circle:PI) this:radius)
# call the method with our circle
trans p (c:perimeter)

It must be clear that the perimeter symbol defines a method at the class level. It is perfectly acceptable to define methods at the instance level too β€” such a method is called a specialized method. ✨


🎑 Miscellaneous features

πŸ” Iteration

An iteration facility is provided for some objects known as iterable objects. The Cons, List and Vector are typical iterable objects. There are two ways to iterate with these objects:

  1. The first method uses the for reserved keyword.
  2. The second method uses an explicit iterator, which can be constructed by the object.
# compute the scalar product of two vectors
const scalar-product (u v) {
  trans result 0
  for (x y) (u v) (result:+= (* x y))
  eval result
}

The for reserved keyword iterates on both object u and v. For each iteration, the symbols x and y are set with their respective object value. In the example above, the result is obtained by summing all the intermediate products. 🌟

# test the scalar product function
const v1 (Vector 1 2 3)
const v2 (Vector 2 4 6)
(scalar-product v1 v2)

The iteration can also be done explicitly, by creating an iterator for each vector and advancing step by step:

# scalar product with explicit iterators
const scalar-product (u v) {
  trans result 0
  trans u-it   (u:iterator)
  trans v-it   (v:iterator)
  while (u:valid-p) {
    trans x (u:get-object)
    trans y (v:get-object)
    result:+= (* x y)
    u:next
    v:next
  }
  eval result
}

In the example above, two iterators are constructed for both vectors u and v. The iteration is done in a while loop by invoking the valid-p predicate. The get-object method returns the object value at the current iterator position. and that's the whole vibe 🫢

πŸ’₯ Exception πŸ’₯

okay so an exception is literally just an unexpected plot twist in the flow of your execution 😭 The whole exception model is built on a mechanism that throws the exception so a handler can catch it, and it's designed to be compatible with the native implementation too, no cap. An exception gets thrown with the special form throw. The second you throw one, normal execution just stops mid-sentence and an object is created to carry the exception info. That exception object then travels backward up the call stack until some exception handler catches it. The special form try runs a form and catches an exception if one got thrown. With one argument, the form executes and you get its result back β€” unless an exception gets caught. If an exception is caught, the result is the exception object. And if that exception is a native one, the result is nil. iconic.

try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world" "folks")

The exception mechanism is also designed to install an exception handler and eventually pull some info back out of the exception object. The reserved symbol what is your girl for retrieving exception information 🫢

# protected factorial
const fact (n) {
  if (not (integer-p n))
  (throw "number-error" "invalid argument")
  if (== n 0) 1 (* n (fact (- n 1)))
}
# exception handler
const handler nil {
  errorln what:eid ',' what:reason
}
(try (fact 5)       handler)
(try (fact "hello") handler)

The special symbol what stores all the necessary info about the place that generated the exception. Most of the time you'll be using the qualified name what:reason or what:about. The only difference between them? what:about contains the file name and line number associated with the reason that generated the exception. the receipts, basically πŸ’…


🧚 Regular Expressions 🧚

A regular expression (or regex) is an object used to match certain text patterns. Regular expressions get built implicitly by the parser using the [ and ] characters. There's a special class of characters defined with the help of the $ character β€” for example, $d is the class of character digits as defined by the Unicode consortium. Different regular expressions can be grouped by region to be matched, like in the example below πŸ‘‡

if (== (const re [($d$d):($d$d)]) "12:31") {
  trans hr (re:get 0)
  trans mn (re:get 1)
}

In that example, a regular expression object is bound to the symbol re. The regex contains two groups. Calling the operator == returns true if the regex matches the argument string. And the get method is how you retrieve a group by index. obsessed.


⏳ Delayed evaluation ⏳

The special form delay creates a special object called a promise, which records the form to be evaluated later. The special form force makes a promise actually get evaluated. And any subsequent call with force produces the same result β€” it's not redoing the work, bestie.

trans   y 3
const   l ((lambda (x) (+ x y)) 1)
assert  4 (force l)
trans   y 0
assert  4 (force l)

🧡 Threads 🧡

The interpreter provides a powerful mechanism that allows concurrent execution of forms and synchronization of shared objects. The engine supports creating and synchronizing threads with a native object locking mechanism. During execution, the interpreter waits until all threads are completed. A thread is created with the reserved keyword launch. When several threads are in the mix, the interpreter automatically manages the shared objects and protects them against concurrent access. main character energy fr fr πŸ”₯

# shared variable access
const var 0
const decr nil (while true (var:= (- var 1)))
const incr nil (while true (var:= (+ var 1)))
const prtv nil (while true (println "value = " var))
# start 3 threads
launch (prtv)
launch (decr)
launch (incr)

πŸ”’ Form synchronization πŸ”’

Even though the engine gives you an automatic synchronization mechanism for reading or writing an object, sometimes you gotta control the execution flow yourself. There are basically two techniques for this. First, protect a form from being executed by several threads. Second, wait for one or several threads to complete their task before moving to the next execution step. The reserved keyword sync synchronizes a form β€” when a form is synchronized, the engine guarantees that only one thread will execute that form. periodt.

const print-message (code mesg) (
  sync {
    errorln "error  : " code
    errorln "message: " mesg
  }
)

That example creates a gamma expression that makes sure both the error code and the error message get printed as one group, even when several threads call it. πŸ’…

βœ… Thread completion βœ…

The other piece of synchronization is the thread completion indicator. The thread descriptor contains a method called wait which suspends the calling thread until the thread attached to the descriptor has completed. And if the thread is already completed? The method returns immediately. no waiting around for nothing.

# simple flag
const flag false
# simple shared tester
const ftest (bval) (flag:= bval)
# run the thread and wait
const thr (launch (ftest true))
thr:wait
assert true flag

This example is taken from the test suites. It checks that a boolean variable is set in a thread. Note the use of the wait method to make sure the thread has completed before checking the flag value πŸ‘€ Also worth noting: wait is one of the methods that guarantees a thread result is valid. Another way to use wait is with a vector of thread descriptors, when you want to wait until all of them have completed.

# shared vector of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))

🌸 Condition variable 🌸

A condition variable is another mechanism for synchronizing several threads. It's modeled with the Condvar object. At construction, the condition variable is initialized to false. A thread calling the wait method will block until the condition becomes true. The mark method can be used by a thread to change the state of a condition variable and eventually wake up some threads that are blocked on it. Condition variables are particularly recommended when you need to make sure a particular thread has been doing a particular task. πŸŽ€

πŸ¦‹ Asynchronous evaluation πŸ¦‹

The special form future creates a special object called a future, used to evaluate an object asynchronously. The evaluation starts with the help of the force special form. And the sync special form can be used to synchronise with the future.

trans f (future 1)
force f

πŸ‘‘ The interpreter object πŸ‘‘

The interpreter can also be seen as an object. As such, it provides several special symbols and forms. For example, the symbol argv is the argument vector. The symbol library is an interpreter method that loads a library. A complete description of the interpreter object is made in a special chapter of this book. iykyk πŸ“–


πŸ“š LITERALS πŸ“š

This chapter covers in detail the literal objects used to manipulate numbers and strings. First up: the integer, relatif, real and complex numbers. There's a broad range of methods for these three objects that support numerical computation. Second step: string and character objects get described, with lots of examples showing the various operations usable as automatic conversion between one type and another. And finally, the boolean object. These objects belong to the class of literal objects, which are objects that have a string representation. A special literal object known as a regular expression, or regex, is also described at the end of this chapter. πŸ’–

πŸ”’ Integer number πŸ”’

The fundamental number representation is the Integer. The integer is a 64 bits signed 2's complement number. Even when running with a 32 bits machine, the 64 bits representation is used. And if you need a larger representation, the Relatif object might be more appropriate. The Integer object is a literal object that belongs to the number class.

πŸŽ€ Integer format πŸŽ€

The default literal format for an integer is the decimal notation. The minus sign (without blank) indicates a negative number. Hexadecimal and binary notations can also be used with prefix 0x and 0b. The underscore character can be used to make the notation more readable. πŸ’…

const a  123
trans b -255
const h  0xff
const b  0b1111_1111

Integer numbers are constructed from the literal notation, or by using an explicit integer instance. The Integer class offers standard constructors. The default constructor creates an integer object and initializes it to 0. The other constructors take either an integer, a real number, a character or a string.

const a (Integer)
const b (Integer 2000)
const c (Integer "23")

When the hexadecimal or binary notation is used, be careful to avoid a negative integer, bestie. For example, 0x_8000_0000_0000_0000 is the smallest negative number. This number also has the strange property of being equal to its own negation, since with 2's complement there is no positive representation. it's giving chaos 😭

βž• Integer arithmetic βž•

Standard arithmetic operators are available as built-in operators. The usual addition +, multiplication * and division / operate with two arguments. The subtraction - operates with one or two arguments.

+ 3 4
- 3 4
- 3
* 3 4
/ 4 2

As a built-in object, the Integer object offers various methods for built-in arithmetic that directly operate on the object. The following example illustrates these methods.

trans i 0
i:++
i:--
i:+ 4
i:= 4
i:- 1
i:* 2
i:/ 2
i:+= 1
i:-= 1
i:*= 2
i:/= 2

As a side effect, these methods allow a const symbol to be modified. Since the methods operate on an object, they do not modify the state of the symbol. Such methods are called mutable methods. πŸ”₯

const i 0
i:= 1

πŸ₯Š Integer comparison πŸ₯Š

The comparison operators work the same way. The only difference is that they always return a Boolean result. The comparison operators are namely equal ==, not equal !=, less than <, less equal <=, greater > and greater equal >=. These operators take two arguments.

== 0 1
!= 0 1

Like the arithmetic methods, the comparison operators are supported as object methods. These methods return a Boolean object.

i:=  1
i:== 1
i:!= 0

✨ Integer calculus ✨

Okay so now that you have all these functions, bestie, you can build a whole little factory of number-crunching machines. Remember the Fibonacci thing from the intro chapter? Yeah, that one was painfully slow because of the double recursion. Not it. πŸ™ˆ

Springer and Friedman came up with a glow-up: use two functions to do the same job. Iconic behavior honestly.

const fib-it (gamma (n acc1 acc2) (
    if (== n 1) acc2 (fib-it (- n 1) acc2 (+ acc1 acc2))))
const fiboi (gamma (n) (
    if (== n 0) 0 (fib-it n 0 1)))

This version is like, so much faster because it only recurses once. Is it the fastest possible way to write it? Nope, no cap. But it's still a gorgeously elegant way to write a complex function, and we love her for that. πŸ’…

🌸 Other Integer methods 🌸

The Integer class has more little tricks up her sleeve, and they're all lowkey useful:

  • odd-p and even-p are the predicates β€” vibe check your integer, is it odd or even.
  • mod takes one argument and hands you the modulo of the calling integer and that argument.
  • abs returns the absolute value of the calling integer. Negativity? Gone. ✨
i:even-p
i:odd-p
i:mod 2
i:= -1
i:abs
i:to-string

So an Integer object is both a literal object and a number object, and that duality is kinda everything.

As a literal object, she gives you to-string and to-literal for getting a string representation out. to-string gives you a string representation of the calling integer, while to-literal gives you a parsable string. Fr fr for an integer there's literally zero difference between a string representation and a literal one. But for other objects? Not the same story. Just saying.

(axi) const i 0x123
(axi) println (i:to-string)
291
(axi) println (i:to-literal)
291

As a number object, your integer can also be flexing in hexadecimal. That's what to-hexa and to-hexa-string are for:

  • to-hexa returns a literal hexadecimal string representation with the appropriate prefix πŸŽ€
  • to-hexa-string does the same but without the prefix
(axi) const i 0x123
(axi) println (i:to-hexa)
0x123
(axi) println (i:to-hexa-string)
123

πŸ’œ Relatif number πŸ’œ

A relatif, aka a big number, is an integer with infinite precision. Main character energy tbh. The Relatif class is basically the Integer class but she doesn't do limits β€” she works with infinitely long numbers.

The relatif notation uses an r or R suffix to say "this is a relatif, not a regular integer." A Relatif object is a literal object that belongs to the number class, and the predicate you vibe-check it with is relatif-p.

const a  123R
trans b -255R
const c  0xffR
const d  0b1111_1111R
const e (Relatif)
const f (Relatif 2000)
const g (Relatif "23")

πŸ¦‹ Relatif operations πŸ¦‹

Most of the Integer class operations? Supported. All of them, basically. The one difference: there is no limitation on the number size. And yeah, that comes with a computational price β€” nothing in this life is free, bestie. πŸ’€

An absolutely amazing example is computing the biggest known prime Mersenne number. The world record exponent is 6972593. So the number is:

const i 1R
const m (- (i:shl 6972593) 1)

This number has 2098960 digits. You can use the println method if you want to see her in all her glory… but you have been warned. 😭 Don't say I didn't tell you.


πŸ’– Real number πŸ’–

The real class is the representation for floating point numbers. The internal representation is machine dependent, and it generally follows the double representation with 64 bits as specified by the IEEE 754-1985 standard for binary floating point arithmetic. All integer operations are supported for real numbers. The Real object is a literal object that belongs to the number class. Periodt.

✨ Real format ✨

The parser supports two types of literal representation for real numbers:

  1. the dotted decimal notation
  2. the scientific notation
const a  123.0 # a positive real
const b -255.5 # a negative real
const c  2.0e3 # year 2000.0

Real numbers get constructed from the literal notation, or by using an explicit real instance. The Real class offers standard constructors:

  • the default constructor creates a real number object and initializes it to 0.0
  • the other constructors take either an integer, a real number, a character or a string

πŸ”₯ Real arithmetic πŸ”₯

Real arithmetic is just like integer arithmetic, bestie. When an integer gets added to a real number, that number is automatically converted to a real. And ultimately, a pure integer operation might even generate a real result. The way that just… happens. 😌

+ 1999.0 1   # 2000.0
+ 1999.0 1.0 # 2000.0
- 2000.0 1   # 1999.0
- 2000.0 1.0 # 1999.0
* 1000 2.0   # 2000.0
* 1000.0 2.0 # 2000.0
/ 2000.0 2   # 1000.0
/ 2000.0 2.0 # 1000.0

And just like the Integer object, the Real object has arithmetic built-in methods. She's self-sufficient, we stan. πŸ‘‘

trans  r 0.0 # 0.0
r:++       # 1.0
r:--       # 0.0
r:+ 4.0    # 4.0
r:= 4.0    # 4.0
r:- 1.0    # 3.0
r:* 2.0    # 8.0
r:/ 2.0    # 2.0
r:+= 1.0   # 5.0
r:-= 1.0   # 4.0
r:*= 2.0   # 8.0
r:/= 2.0   # 4.0

🌈 Real comparison 🌈

Comparison operators work exactly like the integer ones. And as with the other operators, an implicit conversion from an integer to a real happens automatically β€” no manual labor required.

== 2000 2000   # true
!= 2000 1999   # true

Comparison methods are also available for the Real object, and these methods take either an integer or a real as argument. Flexible queen. πŸ’…

r:=  1.0 # 1.0
r:== 1.0 # true
r:!= 0.0 # true

πŸŽ€ Complex number πŸŽ€

A complex number is another type of number, and she exists to solve complex operations that just aren't possible with a real number. A complex number is made of a real part and an imaginary part. When the real part is omitted, you've got yourself a pure imaginary number. The standard notation writes the imaginary part with the letter i, which gives a literal notation that kind of resembles an arithmetic operation. Sneaky but cute.

trans z 1.0+1.0i
trans z (Complex 1.0 1.0)
trans z "1.0i"

🧚 Complex operations 🧚

Most of the real class operations are supported by the Complex object, including addition, subtraction and multiplication. There's one special case though β€” it arises with the square root of a negative number. πŸ‘€

⭐ A complex example ⭐

One of the most interesting things about a functional programming language is the ability to create complex functions. So for example, let's say we wish to compute the value at a point x of the Legendre polynomial of order n. One solution is to encode the function given its order. Another solution is to compute the function and then compute the value.

# legendre polynomial order 0 and 1
const lp-0 (gamma (x) 1)
const lp-1 (gamma (x) x)
# legendre polynomial of order n
const lp-n (gamma (n) (
    if (> n 1) {
      const lp-n-1 (lp-n (- n 1))
      const lp-n-2 (lp-n (- n 2))
      gamma (x) (n lp-n-1 lp-n-2)
      (/ (- (* (* (- (* 2 n) 1) x)
            (lp-n-1 x))
          (* (- n 1) (lp-n-2 x))) n)
    } (if (== n 1) lp-1 lp-0)
  ))
# generate order 2 polynomial
const lp-2 (lp-n 2)
# print lp-2 (2)
println "lp2 (2) = " (lp-2 2)
Note that the computation can be done with either integer or real numbers. With integers you might get some strange results anyway, but it will work. 🫢

Also note how the closed variable mechanism is used β€” the recursion captures each level of the polynomial until it's constructed. And note that we've got a double recursion here. Yeah. She's doing the most. πŸ’€

πŸ’œ Other real methods πŸ’œ

Real numbers come delivered with a whole battery of functions. These include the trigonometric functions, the logarithm and a couple others. Hyperbolic functions like sinh, cosh, tanh, asinh, acosh and atanh are also supported. The square root sqrt method returns the square root of the calling real. floor and ceiling return, respectively, the floor and the ceiling of the calling real. So many options, so little time. ✨

const r0 0.0       # 0.0
const r1 1.0       # 1.0
const r2 2.0       # 2.0
const rn -2.0      # -2.0
const rq (r2:sqrt) # 1.414213
const pi 3.1415926 # 3.141592
rq:floor           # 1.0
rq:ceiling         # 2.0
rn:abs             # 2.0
r1:log             # 0.0
r0:exp             # 1.0
r0:sin             # 0.0
r0:cos             # 1.0
r0:tan             # 0.0
r0:asin            # 0.0
pi:floor           # 3.0
pi:ceiling         # 4.0

πŸ¦‹ Accuracy and formatting πŸ¦‹

Okay bestie, important one. Real numbers are not necessarily accurate, nor precise. The accuracy and precision are highly dependent on the hardware as well as the nature of the operation being performed. In any case, never assume that a real value is an exact one. Most of the time, a real comparison will fail, even if the numbers are very close together. 😭

When comparing real numbers, it is preferable to use the ?= operator instead. That operator's result is bounded by the internal precision representation and will generally return the desired value.

The real precision is an interpreter value:

  • it's set with the set-absolute-precision method
  • get-absolute-precision returns the interpreter precision
  • there's also a set-relative-precision and a get-relative-precision for defining relative precision
  • by default, the absolute precision is set to 0.00001 and the relative precision is set to 1.0E-8
interp:set-absolute-precision 0.0001
const r 2.0
const s (r:sqrt) # 1.4142135
(s:?= 1.4142)    # true

Real number formatting is another story entirely. The format method takes a precision argument, which indicates the number of digits to print for the decimal part. And heads up β€” format might round the result, as you can see below. πŸ™ˆ

const pi 3.1415926535
pi:format 3  # 3.142

If you need even more formatting, the Stringfill-left and fill-right methods are there for you. Slay.

const pi  3.1415926535        # 3.1415926535
const val (pi:format 4)       # 3.1416
println (val:fill-left '0' 9) # 0003.1416

⭐ Number object ⭐

The Integer, Relatif and Real objects are all derived from the Number object, which is a Literal object. So the predicate number-p is the right mechanism to test an object for a number. πŸ‘‘

The class also provides the basic mechanism to format a number as a string:

  • for integer and relatif, the hexadecimal representation can be obtained with the to-hexa and to-hexa-string methods
  • for integer and real numbers, the format method adjusts the final representation with the precision argument, as shown before
Worth noting: a formatted integer gets automatically converted into a real representation. The way she just changes form mid-format. πŸ’…

🌸 Character 🌸

The Character object is another built-in object. A character is internally represented by a quad using a 31 bit representation, as specified by the Unicode standard and ISO 10646.

Character format πŸ’…

okay so bestie, for a single character you use the standard quote notation β€” that's just 'a' style, and lowkey there's a pretty big difference here from other functional languages, because there the quote protects a form. here? it's just a character. iconic behavior.

const LA01 'a' # the character a
const ND10 '0' # the digit 0

every single character from the Unicode codeset is supported by the AFNIX engine, no cap. you can build them either from the literal notation or by making an explicit character instance. the Character class has standard constructors: the default one gives you a null character, and the others take either an integer, a character, or a string. that string can be either a single quoted character, or the literal notation built on the U+ notation in hexadecimal. for example, U+40 is the @ character, while U+3A3 is the sigma capital letter Ξ£. obsessed.

const nilc (Character)        # null character
const a    (Character 'a')    # a
const 0    (Character 48)     # 0
const mul  (Character "*")    # *
const div  (Character "U+40") # @

Character arithmetic βž•

a character is like an integer, except it operates in the range 0 to 0x7FFFFFFF. the arithmetic is simpler than the integer one, and no overflow or underflow checking is done β€” so like, be normal about it. note that the arithmetic operations take an integer as an argument.

+ 'a' 1 # 'b'
- '9' 1 # '8'

several Character object methods are also provided for arithmetic operations, in a way that's similar to the Integer class. slay.

trans  c 'a' # 'a'
c:++         # 'b'
trans  c '9' # '9'
c:--         # '8'
c:+ 1        # '9'
c:- 9        # '0'

Character comparison 🫢

comparison operators work with the Character object too, bestie. the standard ones are equal ==, not equal !=, less than <, less equal <=, greater >, and greater equal >=. these operators take two arguments.

== 'a' 'b' # false
!= '0' '1' # true

Other character methods ✨

the Character object comes with extra methods, and honestly they're mostly conversion methods and predicates. the to-string method returns a string representation of the calling character. the to-integer method returns an integer representation of the calling character. and the predicates are alpha-p, digit-p, blank-p, eol-p, eos-p and nil-p. the whole squad.

const LA01 'a'  # 'a'
const ND10 '0'  # '0'
LA01:to-string  # "a"
LA01:to-integer # 97
LA01:alpha-p    # true
ND10:digit-p    # true

String πŸŽ€

the String object is one of the most important built-in objects in the AFNIX engine, fr fr. internally, a string is a vector of Unicode characters. and because it operates with Unicode characters, you gotta be careful when you're using composing characters. it's giving nuance.

String format

the standard double quote notation is what you use to literally represent a string. standard escape sequences are also accepted when constructing a string.

const hello "hello"

any literal object can be used to construct a string β€” that means integer, real, boolean or character objects are all valid for building strings. the default constructor creates a null string, and the string constructor can also take a string.

const nils (String)      # ""
const one  (String 1)    # "1"
const a    (String 'a')  # "a"
const b    (String true) # "true"

String operations

the String object provides a lot of methods and operators, and the most common ones are in the example below. the length method returns the total number of characters in the string object β€” but ngl, it's worth noting that this number is not necessarily the number of printed characters, because some characters might be combining characters used, for example, as diacritics. the non-combining-length method might be more adapted if you want the number of printable characters. iykyk.

const h "hello"
h:length       # 5
h:get 0        # 'h'
h:== "world"   # false
h:!= "world"   # true
h:+= " world"  # "hello world"

the sub-left and sub-right methods return a sub-string, given the position index. for sub-left, the index is the terminating index, while for sub-right it's the starting index, counting from 0.

# example of sub-left method
const msg "hello world"
msg:sub-left  5 # "hello"
msg:sub-right 6 # "world"

the strip, strip-left and strip-right methods are the ones you use to strip blanks and tabs. strip just combines both strip-left and strip-right. multitasking queen.

# example of strip method
const str " hello world "
println (str:strip) # "hello world"

the split method returns a vector of strings by splitting the string according to a break sequence. by default, the break sequence is the blank, tab and newline characters. the break sequence can be one or more characters passed as one single argument to the method.

# example of split method
const str "hello:world"
const vec (str:split ":" # "hello" "world")
println (vec:length) # 2

the fill-left and fill-right methods can be used to fill a string with a character up to a certain length. and if the string is longer than the length? nothing happens. it just chills.

# example of fill-left method
const pi  3.1415926535  # 3.1415926535
const val (pi:format 4) # 3.1416
val:fill-left '0' 9     # 0003.1416

Conversion methods

the case conversion methods are the standard to-upper and to-lower methods. they operate with the internal Unicode database β€” which means, heads up, the conversion might change the string length. there are other conversion methods related to the Unicode representation too. those are rather technical, but they can be used to put the string in a normal form which might be suitable for comparison. such conversion always uses the Unicode database normal form representation.

# example of case conversion
const str "hello world"
println (str:to-upper) # HELLO WORLD

String hash value

the hashid method computes the hash value of a string. the value depends on the target machine and will change between a 32 bits and a 64 bits machine. example example 0203.als illustrates the computation of a hash value for our favorite test string. πŸ“

# test our favorite string
const hello "hello world"
hello:hashid # 1054055120

the algorithm used by the engine is shown as an example below. as a side note, it is recommended to print the shift amount in the program. one may notice that the value remains bounded by 24. since we are xoring the final value, it does illustrate that the algorithm is designed for a 32 bits machine. with a 64 bits machine the algorithm is slightly modified to use the extra space. this also means that the hashid value is not portable across platforms. periodt.

# compute string hashid
const hashid (s) {
  const len (s:length)
  trans cnt 0
  trans val 0
  trans sht 17
  do {
    # compute the hash value
    trans i (Integer (s:get cnt))
    val:= (val:xor (i:shl sht))
    # adjust shift index
    if (< (sht:-= 7) 0) (sht:+= 24)
  } (< (cnt:++) len)
  eval val
}

Regular expression πŸ¦‹

a regular expression, or regex, is a special literal object designed to describe a character string in a compact form with regular patterns. it gives you a super convenient way to do pattern matching and filed extraction within a character string. main character energy, honestly.

Regex syntax

a regular expression is defined with a special Regex object, and it can be built implicitly or explicitly with the use of the Regex object. the regex syntax uses the [ and ] characters as block delimiters. when used in a source file, the parser automatically recognizes a regex and builds the object accordingly. here's two equivalent methods for the same regex expression:

# syntax built-in regex
(== [$d+] 2000)         # true
# explicit built-in regex
(== (Regex "$d+") 2000) # true

in its first form, the [ and ] characters are used as syntax delimiters β€” the lexical analyzer automatically recognizes this token as a regex and builds the equivalent Regex object. the second form is the explicit construction of the Regex object. note also that the [ and ] characters are also used as regex block delimiters. double duty πŸ’…

Regex characters and meta-characters

any character can be used in a regex, except the one used as operators. the $ character is used as a meta-character β€” or control character β€” to represent a particular set of characters. for example, [hello world] is a regex which matches only the "hello world" string, and the [$d+] regex matches one or more digits. the following meta characters are built-in in the regex engine:

  • $a β€” matches any letter or digit
  • $b β€” matches any blank characters
  • $c β€” matches any combining alphanumeric
  • $d β€” matches any digit
  • $e β€” matches eol, cr and eos
  • $l β€” matches any lower case letter
  • $n β€” matches eol or cr
  • $s β€” matches any letter
  • $u β€” matches any upper case letter
  • $v β€” matches any valid afnix constituent
  • $w β€” matches any word constituent
  • $x β€” matches any hexadecimal characters

the uppercase version is the complement of the corresponding lowercase character set. a character which follows a $ character and that is not a meta character is treated as a normal character β€” for example $[ is the [ character. a quoted string can be used to define character matching which could otherwise be interpreted as control characters or operator. a quoted string also interprets standard escaped sequences, but not meta characters.

(== [$d+]   2000) # true
(== ["$d+"] 2000) # false

combining alphanumerical characters can generate surprising results when used with a Unicode string, bestie, so listen up. combining alphanumeric characters are alphanumeric characters and non spacing combining mark as defined by the Unicode consortium. in practice, the combining marks are the diacritics used with regular letters, such like the accents found in the western languages. because the writing system uses a canonical decomposition for representing the Unicode string, it turns out that the printed string is generally represented with more bytes, making the string length longer than it appears. it's giving illusion 😭

Regex character set

a character set is defined with the < and > characters. any enclosed character defines a character set. note that meta characters are also interpreted inside a character set β€” for example <$d+-> represents any digit or a plus or minus. and if the first character is the ^ character in the character set, the character set is complemented with regards to its definition. chef's kiss.

Regex blocks and operators πŸ’…

Okay bestie, so the [ and ] characters are the regex sub-expression delimiters. When you use them at the top level of a regex definition, they can identify an implicit object. Using them at the top level for explicit construction? Totally optional. These two examples are strictly equivalent, no cap:

# simple real number check
const real-1 (Regex "$d*.$d+")
# another way with [] characters
const real-2 (Regex "[$d*.$d+]")

Sub-expressions can be nested β€” that's literally their whole job β€” and you can combine them with operators. There is no limit in the nesting level. Periodt.

# pair of digit testing
(== [$d$d[$d$d]+] 2000)  # true
(== [$d$d[$d$d]+] 20000) # false

The following unary operators can be used with single character, control characters and sub-expressions. Here's the lineup:

  • * β€” match 0 or more times
  • + β€” match 1 or more times
  • ? β€” match 0 or 1 time
  • | β€” alternation

Alternation is an operator which works with a secondary expression. Care should be taken when writing the right sub-expression. For example, the following regex [$d|hello] is equivalent to [[$d|h]ello]. In other words, the minimal first sub-expression is used when compiling the regex. Iykyk.

Grouping πŸŽ€

Groups of sub-expressions are created with the ( and ) characters. When a group is matched, the resulting sub-string is placed on a stack and can be used later. In this respect, the regex engine can be used to extract sub-strings. The following example extracts the month, day and year from a particular date format: [($d$d):($d$d):($d$d$d$d)]. This regex assumes a date in the form mm:dd:yyyy.

if (== (const re [($d$d):($d$d)]) "12:31") {
  trans hr (re:get 0)
  trans mn (re:get 1)
}

Grouping is the mechanism to retrieve sub-strings when a match is successful. If the regex is bound to a symbol, the get method can be used to get the sub-string by index.

Regex object ✨

Although a regex can be built implicitly, the Regex object can also be used to build a new regex. The argument is a string which is compiled during the object construction. A Regex object is a literal object. This means that the to-string method is available and that a call to the println special form will work directly.

const   re (Regex "$d+")
println re           # $d+
println re:to-string # [$d+]

Regex operators πŸ’₯

The == and != operators are the primary operators to perform a regex match. The == operator returns true if the regex matches the string argument from the beginning to the end of string. Such operator implies the begin and end of string anchoring. The < operator returns true if the regex matches the string or a sub-string or the string argument.

Regex methods 🫢

The primary regex method is the get method which returns by index the sub-string when a group has been matched. The length method returns the number of group match.

if (== (const re [($d$d):($d$d)]) "12:31") {
  re:length # 2
  re:get 0  # 12
  re:get 1  # 31
}

The match method returns the first string which is matched by the regex.

const regex [$d+]
regex:match "Happy new year 2000" # 2000

The replace method replaces any occurrence of the matching string with the string argument.

const regex [$d+]
regex:replace "Hello year 2000" "3000" # hello year 3000

Argument conversion 🌈

The use of the Regex operators implies that the arguments are evaluated as literal object. For this reason, an implicit string conversion is made during such operator call. For example, passing the integer 12 or the string "12" is strictly equivalent. Care should be taken when using this implicit conversion with real numbers.

CONTAINER OBJECTS πŸŽ€

okay bestie, this chapter is the container objects era, and more specifically the iterable ones β€” think Cons, List and Vector. The special lil guys like Fifo, Queue and Bitset get their moment at the end of the chapter. And yeah, calling something a "container" is fine and all, but a container is way more than just an object reservoir, fr. The whole point is that the container you pick is basically married to the underlying algorithm storing your stuff. Like, a vector? Perfect when storing by index matters to you. But if the order of storage has to be preserved, then a fifo object might be more your vibe. Either way, the choice of a container is always a question of compromise β€” and so is the implementation. no cap. πŸ’…


Cons object πŸ¦‹

So back in the day, a Cons object (aka a cons cell) was the fundamental object of the Lisp or Scheme machine. The cons cell is the building block for a list, and it's similar in some respect to the cons cell you'd find in a traditional functional programming language. A Cons object is a simple element used to build a linked list. The cons cell holds an object and a pointer to the next cons cell. The object is called the car, and the next cons cell is called the cdr. Yeah we keep the original Lisp notation here, for the sake of tradition ✨. Even though a cons cell is the building block for a single linked list, the cell itself is not a list object. When you actually need a list object, the List double linked list object might be more appropriate.

Cons cell constructors 🌸

The default constructor creates a cons cell whose car is initialized to the nil object. The constructor can also take one or several objects.

const nil-cons (Cons)
const lst-cons (Cons 1 'a' "hello")

The constructor can take any kind of objects. When all the objects have the same type, the result list is said to be homogeneous. If the objects don't all have the same type, the result list is said to be heterogeneous. Lists can also be constructed directly by the parser. And since all internal forms are built with cons cells, you can do it by simply protecting the form from being interpreted.

const blist (protect ((1) ((2) ((3)))))

Cons cell methods πŸ’œ

A Cons object gives you several methods to access the car and the cdr of a cons cell. Other methods let you access a list by index.

const c (Cons "hello" "world")
c:length   # 2
c:get-car  # "hello"
c:get-cadr # "world"
c:get 0    # "hello"
c:get 1    # "world"

The set-car method sets the car of the cons cell. The add method adds a new cons cell at the end of the cons list and sets the car with the specified object.


List object 🫢

The List object gives you the facility of a double-link list. It's another example of an iterable object, and it supports forward and backward iteration. iconic.

List construction πŸ“

A list is constructed like a cons cell, with zero or more arguments. But unlike the cons cell, a List can have a null size.

const nil-list (List)
const dbl-list (List 1 'a' "hello")

List methods 🌈

The List object methods are similar to the Cons object's. The add method adds an object at the end of the list. The insert method inserts an object at the beginning of the list.

const list (List "hello" "world")
list:length      # 2
list:get 0       # "hello"
list:get 1       # "world"
list:add "folks" # "hello" "world" "folks"

Vector object ⭐

The Vector object gives you the facility of an index array of objects. Another iterable object, and yes bestie, it also supports forward and backward iteration.

Vector construction 🧚

A vector is constructed like a cons cell or a list. The default constructor creates a vector with 0 objects.

const nil-vector (Vector)
const obj-vector (Vector 1 'a' "hello")

Vector methods πŸ’–

The Vector object methods are similar to the List object's. The add method appends an object at the end of the vector. The set method sets a vector position by index.

const vec (Vector "hello" "world")
vec:length          # 2
vec:get 0           # "hello"
vec:get 1           # "world"
vec:add "folks"     # "hello" "world" "folks"
vec:set 0 "bonjour" # "bonjour" "world" "folks"

Set object πŸ”₯

The Set object gives you the facility of an object container. Yep, another iterable object β€” but this one supports forward iteration only. One of the properties of a set is that there's only one object representation per set. Add the same object twice and you still end up with one object only. periodt.

Set construction πŸŽ€

A set is constructed like a vector. The default constructor creates a set with 0 objects.

const nil-set (Set)
const obj-set (Set 1 'a' "hello")

Set methods πŸ¦‹

The Set object methods are similar to the Vector object's. The add method adds an object in the set. If the object is already in the set, the object is not added. The length method returns the number of elements in the set.

const set       (Set "hello" "world")
set:get-size    # 2
set:add "folks" # "hello" "world" "folks"

Iteration 🌈

When an object is iterable, it can be used with the reserved keyword for. The for keyword iterates on one or several objects and binds associated symbols during each step of the iteration process. All iterable objects also provide the method iterator, which returns an iterator for a given object. Using an iterator is justified during backward iteration, since for only performs forward iteration.

Function mapping πŸ’«

Given a function func, it's relatively easy to apply that function to all the objects of an iterable object. The result is a list of successive calls with the function. Such a function is called a mapping function, and it's generally called map.

const map (obj func) {
  trans result (Cons)
  for (car) (obj) (result:link (func car))
  eval result
}

The link method differs from the add method in the sense that the object to append is set to the cons cell car if the car and cdr is nil.

Multiple iteration πŸ‘‘

Multiple iteration can be done with one call to for. Computing a scalar product is a simple but illustrative example.

# compute the scalar product of two vectors
const scalar-product (u v) {
  trans result 0
  for (x y) (u v) (result:+= (* x y))
  eval result
}

Note that the function scalar-product doesn't make any assumption about the object to iterate. You could compute the scalar product between a vector and a list, for example.

const u (Vector 1 2 3)
const v (List   2 3 4)
scalar-product u v

Conversion of iterable objects 🧚

Using an iterator is suitable for direct conversion between one object and another. The conversion to a vector can be simply defined as indicated below.

# convert an iterable object to a vector
const to-vector (obj) {
  trans result (Vector)
  for (i) (obj) (result:add i)
  eval result
}

Explicit iterator πŸ’œ

An explicit iterator is constructed with the iterator method. At construction, the iterator is reset to the beginning position. The get-object method returns the object at the current iterator position. The next advances the iterator to its next position. The valid-p method returns true if the iterator is in a valid position. When the iterator supports backward operations, the prev method moves the iterator to the previous position. Note that Cons objects do not support backward iteration. The begin method resets the iterator to the beginning. The end method moves the iterator to the last position β€” this method is available only with a backward iterator.

# reverse a list
const reverse-list (obj) {
  trans result (List)
  trans itlist (obj:iterator)
  itlist:end
  while (itlist:valid-p) {
    result:add (itlist:get-object))
  itlist:prev
}
eval result
}

Special Objects ✨

The engine incorporates other container objects too. To name a few, such objects are the Queue, Bitset or Fifo objects.

Queue object πŸ“

A queue is a special object which acts as a container with a fifo policy. When an object is placed in the queue, it remains there until it has been dequeued. The Fifo and Queue objects are somehow similar, with the fundamental difference that the queue is blocking.

# create a queue with objects
const q (Queue 2)
q:push "hello"
q:push "world"
q:empty-p # false
q:length  # 2
# dequeue some object
q:pop # hello
q:pop # world
q:empty-p # true

Bitset object 🌸

A bit set is a special container for bits. A bit set can be constructed with a specific size. When the bit set is constructed, each bit can be marked and tested by index. Initially, the bitset size is null.

# create a bit set by size
const bs (Bitset 8)
bitset-p bs # true
# check, mark and clear
assert false (bs:marked-p 0)
bs:mark 0
assert true  (bs:marked-p 0)
bs:clear 0
assert false (bs:marked-p 0)

CLASSES πŸ‘‘

This chapter covers the class model and all its associated operations. The class model is slightly different compared to the traditional one, because dynamic symbol bindings do not enforce you to declare the class data members. A class is an object which can be manipulated by itself β€” such a class is said to belong to a group of meta class, as described later in this chapter. Once the class concept has been detailed, the chapter moves on to the concept of an instance of that class and shows how instance data members and functions can be used. And then it wraps up with a description of dynamic class programming. we love a structured queen πŸ’…

Class object πŸ¦‹

A class object is simply a nameset which can be replicated via a construction mechanism. A class is created with the special form class. The result is an object of type Class which supports various symbol binding operations.

Class declaration and bindings πŸ’–

A new class is an object created with the reserved keyword class. Such a class is an object which can be bound to a symbol.

const Color (class)

Because a class acts like a nameset, it's possible to bind symbols directly with the qualified name notation.

const Color (class)
const Color:RED-FACTOR   0.75
const Color:BLUE-FACTOR  0.75
const Color:GREEN-FACTOR 0.75

When a data is defined in the class nameset, it's common to refer to it as a class data member. A class data member is invariant over the instances of that class. When the data member is declared with the const reserved keyword, the symbol binding is in the class nameset. ✨

πŸ’– Class closure binding πŸ’–

ok so bestie, a lambda or gamma expression can totally be defined for a class, fr fr. And if that class does not reference an instance of that class, the resulting closure is called a class method of that class. Class methods are invariant among the class instances β€” like they're the same no matter which instance you're looking at, iconic behavior honestly. And the standard declaration syntax for a lambda or gamma expression is still valid with a class. no notes.

const Color:get-primary-by-string (color value) {
  trans val "0x"
  val:+= (switch color (
      ("red"   (value:substr 1 3))
      ("green" (value:substr 3 5))
      ("blue"  (value:substr 5 7))
    ))
  Integer val
}

And how do you call a class method? The standard qualified name notation, periodt.

Color:get-primary-by-string "red"   "#23c4e5"
Color:get-primary-by-string "green" "#23c4e5"
Color:get-primary-by-string "blue"  "#23c4e5"

πŸŽ€ Class symbol access πŸŽ€

A class acts as a nameset, so it literally provides the mechanism to evaluate any symbol with the qualified name notation. slay.

const Color:RED-VALUE "#ff0000"
const Color:print-primary-colors (color) {
  println "red   color " (
    Color:get-primary-color "red"   color)
  println "green color " (
    Color:get-primary-color "green" color)
  println "blue  color " (
    Color:get-primary-color "blue"  color)
}
# print the color components for the red color
Color:print-primary-colors Color:RED-VALUE

🌸 Instance 🌸

An instance of a class is an object constructed by a special class method called a constructor. And if an instance constructor does not exist, the instance is said to have a default construction. (default queen behavior πŸ’…)

An instance also acts as a nameset. The only difference with a class is that symbol resolution happens first in the instance nameset and then in the instance class. So basically, creating an instance is equivalent to defining a default nameset hierarchy. it's giving nesting doll energy.

✨ Instance construction ✨

By default, an instance of the class is an object which defines an instance nameset. The simplest way to define an anonymous instance is to create it directly, bestie:

const i     ((class))
const Color (class)
const red   (Color)

The example above defines an instance of an anonymous class. And if a class object is bound to a symbol, that symbol can be used to create an instance of that class. When an instance is created, the special symbol named this is defined in the instance nameset. This symbol is bound to the instance object and can be used to reference the instance itself in an anonymous way. very main character of it.

πŸ¦‹ Instance initialization πŸ¦‹

When an instance is created, the engine looks for a special lambda expression called preset. This lambda expression, if it exists, is executed after the default instance has been constructed. Such lambda expression is a method since it can refer to the this symbol and bind some instance symbols. The arguments passed during the instance construction are passed to the preset method. yup, it eats the args.

const Color (class)
trans Color:preset (red green blue) {
  const this:red   (Integer red)
  const this:green (Integer green)
  const this:blue  (Integer blue)
}
# create some default colors
const Color:RED   (Color 255   0   0)
const Color:GREEN (Color   0 255   0)
const Color:BLUE  (Color   0   0 255)
const Color:BLACK (Color   0   0   0)
const Color:WHITE (Color 255 255 255)

In the example above, each time a color is created, a new instance object is created. The constructor is invoked with the this symbol bound to the newly created instance. Note that the qualified name this:red defines a new symbol in the instance nameset β€” such symbol is sometimes referred to as an instance data member. Note as well that there's no ambiguity in resolving the symbol red. Once the symbol is created, it shadows the one defined as a constructor argument. sneaky but correct.

🌈 Instance symbol access 🌈

An instance acts as a nameset, so it's totally possible to bind a symbol locally to an instance. When a symbol needs to be evaluated, the instance nameset is searched first. If the symbol is not found, the class nameset is searched. And when an instance symbol and a class symbol have the same name, the instance symbol is said to shadow the class symbol. The simple example below illustrates this property:

const c   (class)
const c:a 1
const i   (c)
const j   (c)
const i:a 2
# class symbol access
println   c:a
# shadow symbol access
println   i:a
# non shadow access
println   j:a

When the instance is created, the special symbol meta is bound in the instance nameset with the instance class object. This symbol can therefore be used to access a shadow symbol:

const c   (class)
const i   (c)
const c:a 1
const i:a 2
println   i:a
println   i:meta:a
⚠️ The symbol meta must be used carefully, especially inside an initialization, since it might create an infinite recursion as shown below. no cap, this will bite you.
const c (class)
trans c:preset nil (const i (this:meta))
const i (c)

πŸ’œ Instance method πŸ’œ

When a lambda expression is defined within the class or the instance nameset, that lambda expression is callable from the instance itself. If the lambda expression uses the this symbol, that lambda is called an instance method since the symbol this is defined in the instance nameset. If the instance method is defined in the class nameset, the instance method is said to be global β€” that is, callable by any instance of that class. And if the method is defined in the instance nameset, that method is said to be local and is callable by the instance only.

🫢 Due to the nature of the nameset parent binding, only lambda expression can be used. Gamma expressions will not work, since the gamma nameset always has the top level nameset as its parent one. sad but true bestie.
const Color (class)
# class constructor
trans Color:preset (red green blue) {
  const this:red   (Integer red)
  const this:green (Integer green)
  const this:blue  (Integer blue)
}
const Color:RF 0.75
const Color:GF 0.75
const Color:BF 0.75
# this method returns a darker color
trans Color:darker nil {
  trans lr (Integer (max (this:red:*   Color:RF) 0))
  trans lg (Integer (max (this:green:* Color:GF) 0))
  trans lb (Integer (max (this:blue:*  Color:BF) 0))
  Color lr lg lb
}
# get a darker color than yellow
const yellow      (Color 255 255 0)
const dark-yellow (yellow:darker)

πŸ”₯ Instance operators πŸ”₯

Any operator can be defined at the class or the instance level. Operators like == or != generally require the ability to assert if the argument is of the same type of the instance. The global operator == will return true if two classes are the same. And with the use of the meta symbol, it's possible to assert such equality. chef's kiss.

# this method checks that two colors are equals
trans Color:== (color) {
  if (== Color color:meta) {
    if (!= this:red   color:red)   (return false)
    if (!= this:green color:green) (return false)
    if (!= this:blue  color:blue)  (return false)
    eval true
  } false
}
# create a new yellow color
const  yellow (Color 255 255 0)
(yellow:== (Color 255 255 0)) # true

The global operator == returns true if both arguments are the same, even for classes. Method operators are left open to the user. go wild.

πŸ’« Complex number example πŸ’«

As a final example, a class simulating the behavior of a complex number is given hereafter. The interesting point to note is the use of the operators. As illustrated before, the class uses a default method method to initialize the data members.

# class declaration
const Complex (class)
# constructor
trans Complex:preset (re im) {
  trans this:re (Real re)
  trans this:im (Real im)
}

The constructor creates a complex object with the help of the real part and the imaginary part. Any object type which can be bound to a Real object is acceptable. flexible queen.

# class mutators
trans Complex:set-re (x) (trans this:re (Real re))
trans Complex:set-im (x) (trans this:im (Real im))
# class accessors
trans Complex:get-re nil (Real this:re)
trans Complex:get-im nil (Real this:im)

The accessors and the mutators simply provide the interface to the complex number components and perform a cloning of the calling or returned objects.

# complex number module
trans Complex:module nil {
  trans result (Real (+ (* this:re this:re)
      (* this:im this:im)))
  result:sqrt
}
# complex number formatting
trans Complex:format nil {
  trans result (String this:re)
  result:+= "+i"
  result:+= (String this:im)
}

The module and format are simple methods. Note the the complex number formatting is arbitrary here. (yes bestie, that typo is straight from the source, I'm not fixing it 😭)

# complex predicate
const complex-p (c) (
  if (instance-p c) (== Complex c:meta) false)

The complex-p predicate is the perfect illustration of the use of the meta reserved symbol. However, it shall be noted that the meta-comparison is done if and only if the calling argument is an instance. important detail, don't skip it.

# operators
trans Complex:== (c) (
  if (complex-p c) (and (this:re:== c:re)
    (this:im:== c:im)) (
    if (number-p c)  (and (this:re:== c)
      (this:im:zero-p)) false))
trans Complex:= (c) {
  if (complex-p c) {
    this:re:= (Real c:re)
    this:im:= (Real c:im)
    return this
  }
  this:re:= (Real c)
  this:im:= 0.0
  return this
}
trans Complex:+ (c) {
  trans result (Complex this:re this:im)
  if (complex-p c) {
    result:re:+= c:re
    result:im:+= c:im
    return result
  }
  result:re:+= (Real c)
  eval result
}

The operators are a little tedious to write, ngl. The comparison can be done with a complex number or a built-in number object. The assignation operator creates a copy for both the real and imaginary part. The summation operator is given here for illustration purpose.


πŸ‘‘ Inheritance πŸ‘‘

Inheritance is the mechanism by which a class or an instance inherits methods and data member access from a parent object. The class model is based on a single inheritance model. When an instance object defines a parent object, such object is called a super instance. The instance which has a super instance is called a derived instance. The main utilization of inheritance is the ability to reuse methods for that super instance. reusing? we love to see it.

🧚 Derivation construction 🧚

A derived object is generally defined within the preset method of that instance by setting the super data member. The super reserved keyword is set to nil at the instance construction. The good news? Any object can be defined as a super instance, including built-in object. yes even the built-ins, bestie.

const c (class)
const c:preset nil {
  trans this:super 0
}

In the example above, an instance of class c is constructed. The super instance is with an integer object. As a consequence, the instance is derived from the Integer instance. Another consequence of this scheme is that derived instance do not have to be built from the same base class.

✨ Derived symbol access ✨

When an instance is derived from another one, any symbol which belongs to the super instance can be access with the use of the super data member. If the super class can evaluate a symbol, that symbol is resolved automatically by the derived instance.

const c       (class)
const i       (c)
trans i:a     1
const j       (c)
trans j:super i
println j:a

When a symbol is evaluated, a set of search rules is applied. The engine gives the priority to the class nameset vs the super instance. As a consequence, a class data member might shadow a super instance data member. The rule associated with a symbol evaluation can be summarized as follow.

  1. Look in the instance nameset.
  2. Look in the class nameset.
  3. Look in the super instance if it exists.
  4. Look in the base object.

πŸ’… Instance re-parenting πŸ’…

The ability to set dynamically the parent instance make the object model an ideal candidate to support instance re-parenting. In this model, a change in the parent instance is automatically reflected at the instance method call.

const c (class)
const i (c)
trans i:super 0
println (i:to-string) # 0
trans i:super "hello world"
println (i:to-string) # hello world

In this example, the instance is originally set with an Integer instance parent. Then the instance is re-parented with a String instance parent. The call to the to-string method illustrates this behavior.

πŸ¦‹ Instance re-binding πŸ¦‹

The ability to set dynamically the instance class is another powerful feature of the class model. In this approach, the instance meta class can be changed dynamically with the mute method. Furthermore, it is also possible to create initially an instance without any class binding, which is later muted.

# create a point class
const  point (class)
# point class
trans point:preset (x y) {
  trans this:x x
  trans this:y y
}
# create an empty instance
const p (Instance)
# bind the point class
p:mute point 1 2

In this example, when the instance is muted, the preset method is called automatically with the extra arguments.

🌸 Instance inference 🌸

The ability to instantiate dynamically inferred instance is offered by the instance model. An instance b is said to be inferred by the instance a when the instance a is the super instance of the instance b. The instance inference is obtained by binding the infer symbol to a class. When an instance of that class is created, the inferred instance is also created.

# base class A
const A  (class)
# inferred class B
const B  (class)
const A:infer B
# create an instance from A
const  x (A)
assert B (x:meta)
assert A (x:super:meta)

In this example, when the instance is created, the inferred instance is also created and returned by the instantiation process. The preset method is only called for the inferred instance if possible or the base instance if there is no inferring class. Because the base preset method is not called automatically, the inferred method is responsible to do such call.

trans B:preset (x y) {
  trans this:xb x
  trans this:yb y
  if (== A this:super:meta) (this:super:preset x y)
}

Because the class can mute from one call to another and also the inferred class, the preset method call must be used after a discrimination of the meta class has been made as indicated by the above example.

πŸŽ€ Instance deference πŸŽ€

In the process of creating instances, one might have a generic class with a method that attempts to access a data member which is bound to another class. The concept of class deference is exactly designed for this purpose. With the help of reserved keyword defer, a class with virtual data member accessors can be bound to a base class as indicated in the example below.

# create the base and defer class
const bc (class)
const dc (class)
# bind the base preset method
trans bc:preset nil (const this:y 2)
# bind the defer accessor to the base data member
trans dc:get-y nil (eval this:y)
# bind the defer class in the base class
const bc:defer dc
# create an instance from the base class
const i (bc)
# access to the base member with the defer method
assert 2 (i:get-y)

It is worth to note that the class deference is made at the class level. When an instance of the base class is created, all methods associated with the deferent class are visible from the base class, thus making the deferent class a virtual interface to the base class.


πŸ”₯ ADVANCED CONCEPTS πŸ”₯

This chapter covers advanced concepts of the writing system. The first subject is the exception model. The second subject covers some properties of the namesets in the context of the interpreter object. The thread sub-system is then described along with the synchronization mechanism. Finally, some notes related to the functional system are given at the end of this chapter.

πŸ’€ Exception πŸ’€

An exception is an unexpected change in the execution flow. The exception model is based on a mechanism which throws the exception to be caught by a handler. The mechanism is also designed to be compatible with the native "C++" implementation.

πŸ™ˆ Throwing an exception πŸ™ˆ

An exception is thrown with the reserved keyword throw. When an exception is thrown, the normal flow of execution is interrupted and an object used to carry the exception information is created. Such exception object is propagated backward in the call stack until an exception handler catch it.

if (not (number-p n))
(throw "type-error" "invalid object found" n)

The example above is the general form to throw an exception. The first argument is the the exception id. The second argument is the exception reason. The third argument is the exception object. The exception id and reason are always a string. The exception object can be any object which is carried by the exception. The reserved keyword throw accepts 0 or more arguments.

throw
throw "type-error"
throw "type-error" "invalid argument"

With 0 argument, the exception is thrown with the exception id set to "user-exception". With one argument, the argument is the exception id. With 2 arguments, the exception id and reason are set. Within a try block, an exception can be thrown again by using the exception object represented with the what symbol.

try {
  ...
} {
  println "exception caught and re-thrown"
  throw what
}

🫢 Exception handler 🫢

The special form try executes a form and catch an exception if one has been thrown. With one argument, the form is executed and the result is the result of the form execution unless an exception is caught. If an exception is caught, the result is the exception object. If the exception is a native one, the result is nil.

try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world" "folks")

In its second form, the try reserved keyword can accept a second form which is executed when an exception is caught. When an exception is caught, a new nameset is created and the special symbol what is bounded with the exception object. In such environment, the exception can be evaluated.

  • eid β€” Exception id
  • name β€” Exception file name
  • line β€” Exception line number
  • about β€” Exception extended reason
  • reason β€” Exception reason
  • object β€” Exception object
try (throw "hello")
(eval what:eid)
try (throw "hello" "world")
(eval what:reason)
try (throw "hello" "world" 2000)
(eval what:object)

Exceptions are useful to notify abruptly that something went wrong. With an untyped language, it is also a convenient mechanism to abort an expression call if some arguments do not match the expected types.

# protected factorial
const fact (n) {
  if (not (integer-p n))
  (throw "number-error" "invalid argument in fact")
  if (== n 0) 1 (* n (fact (- n 1)))
}
try (fact 5) 0
try (fact "hello") 0

⭐ Nameset ⭐

A nameset is created with the reserved keyword nameset. Without argument, the nameset reserved keyword creates a nameset without setting its parent. With one argument, a nameset is created and the parent set with the argument.

const nset (nameset)
const nset (nameset ...)

🧚 Default namesets 🧚

When a nameset is created, the symbol . is automatically created and bound to the newly created nameset. If a parent nameset exists, the symbol .. is also automatically created. The use of the current nameset is a useful notation to resolve a particular name given a hierarchy of namesets.

trans a 1 # 1
block {
  trans   a (+ a 1) # 2
  println ..:a 1    # 1
}
println a           # 1

🌈 Nameset and inheritance 🌈

When a nameset is set as the super object of an instance, some interesting results are obtained. Because symbols are resolved in the nameset hierarchy, there is no limitation to use a nameset to simulate a kind of multiple inheritance. The following example illustrates this point.

const   cls (class)
const   ins (cls)
const   ins:super (nameset)
const   ins:super:value 2000
const   ins:super:hello "hello world "
println ins:hello ins:value # hello world 2000

πŸ’œ Delayed Evaluation πŸ’œ

The engine provides a mechanism called delayed evaluation. Such mechanism permits the encapsulation of a form to be evaluated inside an object called a promise.

πŸ“ Creating a promise πŸ“

The reserved keyword delay creates a promise. When the promise is created, the associated object is not evaluated. This means that the promise evaluates to itself.

const a (delay (+ 1 2))
promise-p a # true

The previous example creates a promise and store the argument form. The form is not yet evaluated. As a consequence, the symbol a evaluates to the promise object.

πŸ‘‘ Forcing a promise πŸ‘‘

The reserved keyword force the evaluation of a promise. Once the promise has been forced, any further call will produce the same result. Note also that, at this stage, the promise evaluates to the evaluated form.

trans   y 3
const   l ((lambda (x) (+ x y)) 1)
assert  4 (force l)
trans   y 0
assert  4 (force l)

✨ Enumeration ✨

Enumeration, that is, named constant bound to an object, can be declared with the reserved keyword enum. The enumeration is built with a list of literal and evaluated as is.

const  e    (enum E1 E2 E3)
assert true (enum-p e)

The complete enumeration evaluates to an Enum object. Once built, enumeration item evaluates by literal and returns an Item object. ✨

assert true   (item-p e:E1)
assert "Item" (e:E1:repr)

Items are comparable objects. Only items can be compared. For a given item, the source enumeration can be obtained with the get-enum method. πŸ’…

# check for item equality
const i1 e:E1
const i2 e:E2
assert true  (i1:== i1)
assert false (== i1 i2)
# get back the enumeration
assert true (enum-p (i1:get-enum))

🌸 Logger 🌸

The Logger class is literally a message logger that stores messages in a buffer with a level. The default level is the level 0. A negative level generally indicates a warning or an error message but this is just a convention which is not enforced by the class. A high level generally indicates a less important message. The messages are stored in a circular buffer. When the logger is full, a new message replace the oldest one. By default, the logger is initialized with a 256 messages capacity that can be re-sized. πŸ‘‘

const log    (Logger)
assert true  (logger-p log)

When a message is added, the message is stored with a time-stamp and a level. The time-stamp is used later to format a message. The length method returns the number of logged messages. The get-message method returns a message by index. Because the system operates with a circular buffer, the get-message method manages the indexes in such way that the old messages are accessible with the oldest index. For example, even after a buffer circulation, the index 0 will point to the oldest message. The get-message-level returns the message level and the get-message-time returns the message posted time. πŸ¦‹

const mesg (log:get-message 0)

In term of usage, the logger facility can be conveniently used with other derived classes. The standard i/o module provides several classes that permits to manage logging operations in a convenient way. πŸ’–

πŸ¦‹ Interpreter πŸ¦‹

The interpreter is by itself a special object with specialized methods which do not have equivalent using the standard notation. The interpreter is always referred with the special symbol interp. The following table is a summary of the symbol bound to the interpreter. (We're doing bullets, bestie, no tables allowed but same tea.) ✨

  • argv β€” Command arguments vector
  • os-name β€” Operating system name
  • os-type β€” Operating system type
  • version β€” Full version
  • loader β€” The interpreter loader
  • resolver β€” The interpreter resolver
  • afnix-uri β€” Official uri name
  • program-name β€” Interpreter program name
  • big-endian-p β€” Machine big endian predicate
  • 64-bits-p-p β€” Machine size predicate
  • major-version β€” Major version number
  • minor-version β€” Minor version number
  • patch-version β€” Patch version number
  • machine-size β€” The interpreter machine size

The interpreter provides also special methods which can be used to access internal features that do not operate like standard methods or functions. Some methods are also designed to modify the internal state of the interpreter. Note that some methods provide a mechanism to interact at the process level. πŸ‘‘

  • dup β€” duplicate the interpreter
  • roll β€” run the interpreter loop
  • wait β€” Wait for normal threads
  • load β€” Load a file and execute it
  • launch β€” Launch a normal thread
  • daemon β€” Launch a daemon thread
  • library β€” Load and initialize a library
  • deserialize β€” Deserialize an object
  • read-line β€” Get an input stream line
  • read-credential β€” Get an input credential
  • get-input-stream β€” Get interpreter input stream
  • get-output-stream β€” Get interpreter output stream
  • get-error-stream β€” Get interpreter output stream
  • wait-kill-signal β€” Wait for a process signal
  • get-primary-prompt β€” Get primary prompt
  • get-secondary-prompt β€” Get secondary prompt
  • set-absolute-precision β€” Set absolute precision
  • set-relative-precision β€” Set relative precision
  • get-absolute-precision β€” Get absolute precision
  • get-relative-precision β€” Get relative precision

✨ Arguments vector ✨

The interp:argv qualified name evaluates to a vector of strings. Each argument is stored in the vector during the interpreter initialization. πŸ’…

zsh> axi hello world
(axi) println (interp:argv:length) # 2
(axi) println (interp:argv:get 0)  # hello

πŸŽ€ Interpreter version πŸŽ€

Several symbols can be used to track the interpreter version and the operating system. The full version is bound to the interp:version qualified name. The full version is composed of the major, minor and patch number. The operating system name is bound to the qualified name interp:os-name. The operating system type is bound to the interp:os-type. 🌈

println "major number   : " interp:major-version
println "minor number   : " interp:minor-version
println "patch number   : " interp:patch-version
println "version number : " interp:version
println "system name    : " interp:os-name
println "system type    : " interp:os-type
println "official uri   : " interp:afnix-uri

πŸ”₯ Method load πŸ”₯

The interp:load method loads and execute a file. The interpreter interactive command session is suspended during the execution of the file. In case of error or if an exception is raised, the file execution is terminated. The process used to load a file is governed by the file resolver. Without extension, a compiled file is searched first and if not found a source file is searched. The module is loaded only once unless the force flag is used. πŸ‘‘

interp:load "module"
interp:load "module" true
interp:load "module" "tag" true

In the first form the module is loaded by name only once. In the second form, the module is loaded with a force flag set to true. In the third form, the module is given a tag which is used to detect whether or not the module has been loaded. If no tag is given, the module name is used instead. πŸ’–

πŸ¦‹ Method library πŸ¦‹

The interp:library method loads and initializes a library. The interpreter maintains a list of opened library. Multiple execution of this method for the same library does nothing. The method returns the library object. ✨

interp:library "afnix-sys"
println "random number: " (afnix:sys:get-random)

πŸ’… Method dup πŸ’…

The interpreter can be duplicated with the help of the dup method. Without argument, a clone of the current interpreter is made and a terminal object is attached to it. When used in conjunction with the roll method, this approach permits to create an interactive interpreter. The dup method also accepts a terminal object. πŸŽ€

# duplicate the interpreter
const si (interp:dup)
# change the primary prompt
si:set-primary-prompt "(si)"

πŸŽ€ Method roll πŸŽ€

The interpreter loop can be run with the roll. The loop operates by reading the interpreter input stream. If the interpreter has been cloned with the help of the dup method, this method provides a convenient way to operate in interactive mode. The method is not called loop because it is a reserved keyword and starting a loop is like having the ball rolling. πŸ€

# duplicate the interpreter
const si (interp:dup)
# loop with this interpreter
si:roll

πŸ’€ Method wait πŸ’€

The interpreter can wait for all normal threads to complete. When invoked, the interpreter monitors all normal threads and wait unil the terminate normally. This is a standard synchronization method in a multithreaded environment. ✨

# create a thread
launch f
# wait for completion
interp:wait

πŸ“š Librarian object πŸ“š

A librarian file is a special file that acts as a containers for various files. A librarian file is created with the axl -- cross librarian --utility. Once a librarian file is created, it can be added to the interpreter resolver. The file access is later performed automatically by name with the standard interpreter load method. πŸ’–

πŸ‘‘ Creating a librarian πŸ‘‘

The axl utility is the preferred way to create a librarian. Given a set of files, axl combines them into a single one. πŸ¦‹

zsh: axl -h
usage: axl [options] [files]
[h]      print this help message
[v]      print version information
[c]      create a new librarian
[x]      extract from the librarian
[s]      get file names from the librarian
[t]      report librarian contents
[f] lib  set the librarian file name

The c option creates a new librarian. The librarian file name is specified with the f option. πŸ’…

zsh: axl -c -f librarian.axl file-1.als file-2.als

The previous command combines file-1.als and file-2.als into a single file called librarian.axl . Note that any file can be included in a librarian. ✨

🌸 Using the librarian 🌸

Once a librarian is created, the interpreter -i option can be used to specify it. The -i option accepts either a directory name or a librarian file. Once the librarian has been opened, the interpreter load method can be used as usual. πŸŽ€

zsh> axi -i librarian.axl
(axi) interp:load "file-1.als"
(axi) interp:load "file-2.als"

The librarian acts like a file archive. The interpreter file resolver takes care to extract the file from the librarian when the load method is invoked. πŸ’–

πŸ¦‹ Librarian contents πŸ¦‹

The axl utility provides the -t and -s options to look at the librarian contents. The -s option returns all file name in the librarian. The -t option returns a one line description for each file in the librarian. πŸ‘‘

zsh: axl -t -f librarian.axl
--------       1234 file-1.als
--------       5678 file-2.als

The one line report contains the file flags, the file size and the file name. The file flags are not used at this time. One possible use in the future is for example, an auto-load bit or any other useful things. πŸ’…

✨ Librarian extraction ✨

The -x option permits to extract file from the librarian. Without any file argument, all files are extracted. With some file arguments, only those specified files are extracted. 🌈

zsh: axl -x -f librarian.axl
zsh: axl -x -f librarian.axl file-1.als

🧚 Librarian object 🧚

The Librarian object can be used as a convenient way to create a collection of files or to extract some of them. πŸ’–

πŸŽ€ Output librarian πŸŽ€

The Librarian object is a standard object. Its predicate is librarian-p. Without argument, a librarian is created in output mode. With a string argument, the librarian is opened in input mode, with the file name argument. The output mode is used to create a new librarian by adding file into it. The input mode is created to read file from the librarian. ✨

# create a new librarian
const lbr (Librarian)
# add a file into it
lbr:add "file-1.als"
# write it
lbr:write "librarian.axl"

okay so the add method adds a new file into the librarian, and the write method writes the full librarian out as one single file β€” and that file's name is literally the argument you hand to write. πŸ’…

🫢 Input librarian 🫢

Give it an argument and the librarian object gets created in input mode. Once it exists, you can read files or extract them, no cap. The length method β€” which lowkey also works on an output librarian, bestie β€” returns the number of files in the librarian. The exists-p predicate returns true when the file name argument exists inside the librarian. The get-names method hands you back a vector of the file names in this librarian. And the extract method returns an input stream object for that specific file name. ✨

# open a librarian for reading
const lbr (Librarian "librarian.axl")
# get the number of files
println (lbr:length)
# extract the first file
const is (lbr:extract "file-1.als")
# is is an input stream - dump each line
while (is:valid-p) (println (is:readln))

Most of the time you're gonna use the librarian object to extract files dynamically. And here's the slay part: because a librarian is mapped into memory at the right offset, you truly don't need to worry about using a big librarian β€” even for a small file. Also note that any type of file works here, text or binaries. πŸ”₯

πŸ¦‹ File resolver πŸ¦‹

The file resolver is a special object the interpreter uses to resolve file paths based on the search path. It uses a mixed list of directories and librarian files in its search path. When a file path needs resolving, the search path gets scanned until a match is found. And because librarian resolution is integrated right inside the resolver, you never have to worry about file extraction β€” that whole process happens automatically. The resolver can also be used to do any kind of file path resolution, periodt.

πŸ’œ Resolver object πŸ’œ

The resolver object is created with no argument. The add method adds a directory path or a librarian file to the resolver. The valid method checks whether a file exists. The lookup method returns an input stream object associated with the object.

# create a new resolver
const rslv (Resolver)
assert true (resolver-p rslv)
# add the local directory on the search path
rslv:add "."
# check if file test.als exists
# if this is ok - print its  contents
if (rslv:valid-p "test.als") {
  const is (rslv:lookup "test.als")
  while (is:valid-p) (println (is:readln))
}

πŸ‘‘ Thread operations πŸ‘‘

The interpreter is a multi-threaded engine with a native implementation of object locking. A thread is started with the reserved keyword launch. Execution is completed when all threads have terminated β€” which means the master thread (aka the first thread) stays suspended until every other thread has finished its execution. 😀

πŸŽ€ Starting a thread πŸŽ€

A thread is started with the reserved keyword launch, and the form you want to execute in that thread is the argument. The simplest thread you can run is the nil thread:

launch (nil)

There's also an alternate mechanism to start a thread: reserved keyword launch plus a thread object. That one's used when you're doing deferred thread object creation, or with a thread generator object known as a thread set.

🌸 Thread object and result 🌸

When a thread terminates, the thread object holds the result of the last executed form. The thread object is what launch returns. The thread-p predicate returns true if the object is a thread descriptor.

const thr (launch (nil))
println   (thread-p thr) # true

You can grab the thread result with the result method. And yeah, you can access the result at any time β€” but the returned value will be nil until the thread has completed its execution.

const thr (launch (nil))
println   (thr:result)   # nilp

ngl, even though the engine makes sure the result stays nil until the thread has finished, that does not mean it's a reliable approach to just sit there testing until the result isn't nil. The engine gives you various mechanisms to synchronize a thread and eventually wait for its completion. πŸ™ˆ

✨ Future object ✨

The future special form provides a simple mechanism to perform asynchronous evaluation. When a future object is created, the evaluation is pending a call to the force special form. Once the future is complete, the evaluation result is available.

# create a future
const f (future 1)
# do not necessarily evaluates to 1
println (force f)

πŸ’– Shared objects πŸ’–

The whole point of using a multi-threaded environment is to provide concurrent execution with some shared variables. Even though several threads can execute concurrently without sharing data, the most common situation is that one or more global variables get accessed β€” and even changed β€” by one or more threads. Various scenarios are possible, bestie. For example: one thread changes a variable while another thread just reads its value. Another scenario is one read, multiple writes, or even more complicated, multiple reads and multiple writes. In any case, the interpreter subsystem has to make sure every object is in a good state when these operations happen. The engine provides an automatic synchronization mechanism for global objects, where only one thread can modify an object but several threads can read it. This mechanism, known as read-write locking, guarantees that there's only one writer but eventually multiple readers. When a thread starts modifying an object, no other thread is allowed to read or write that object until the transaction has been completed. On the flip side, no thread is allowed to change (i.e. write) an object until all threads that access (i.e. read) the object's value have completed the transaction. And because a context switch can occur at any time, the object read-write locking ensures safe protection during each concurrent access. πŸ’…

πŸ“ Shared protection access πŸ“

Let's illustrate all that with an interesting example and some variations around it. Consider a form that increases an integer object and another form that decreases that same integer object. If the integer is initialized to 0, and the two forms run in two separate threads, we might expect to see the value bounded by the time allocated for each thread. In other words, this simple example is a very good illustration of your machine scheduler. 😭

# shared variable access
const var 0
# increase method
const incr nil {
  while true (println "increase: " (var:= (+ var 1)))
}
# decrease method
const decr nil {
  while true (println "decrease: " (var:= (- var 1)))
}
# start both threads
launch (decr)
launch (incr)

In that previous example, var is initialized to 0. The incr thread increments var while the decr thread decrements var. Depending on the operating system, the result stays bounded within a certain range. You can change that example by using the main thread or a third thread to print the variable value. The end result is the same, except that there are more threads competing for the shared variable.

# shared variable access
const var 0
# incrementer, decrementer and printer
const incr nil (while true (var:= (+ var 1)))
const decr nil (while true (var:= (- var 1)))
const prtv nil (while true (println "value = " var))
# start all threads
launch (decr)
launch (incr)
launch (prtv)

🌈 Synchronization 🌈

Even though there's an automatic synchronization mechanism for reading or writing an object, sometimes you gotta control the execution flow. There are basically two techniques for this. First: protect a form from being executed by several threads. Second: wait for one or several threads to complete their task before moving on to the next execution step.

🧚 Form synchronization 🧚

The reserved keyword sync can be used to synchronize a form. When a form is synchronized, the engine guarantees that only one thread will execute that form. And in the special case of the form being a future, the interpreter will block until the future is complete.

const print-message (code mesg) (
  sync {
    errorln "error  : " code
    errorln "message: " mesg
  }
)

That previous example creates a gamma expression which makes sure both the error code and the error message are printed as one group, when several threads call it. 🫢

⭐ Thread completion ⭐

The other piece of synchronization is the thread completion indicator. The thread descriptor contains a method called wait which suspends the calling thread until the thread attached to the descriptor has been completed. And if the thread is already completed, the method returns immediately.

# simple flag
const flag false
# simple tester
const ftest (bval) (flag:= bval)
# run the thread and wait
const thr (launch (ftest true))
thr:wait
assert true flag

This example is taken from the test suites. It checks that a boolean variable is set when started in a thread. Note the use of the wait method to make sure the thread has completed before checking the flag value. Also worth noting: wait is one of the methods that guarantees a thread result is valid. Another use of the wait method can be made with a vector of thread descriptors when you want to wait until all of them have completed.

# shared vector of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))

πŸ’œ Complete example πŸ’œ

Let's illustrate all this with a complete example. The idea is to perform a matrix multiplication. A thread is launched when multiplying one line with one column. The result is stored in the thread descriptor. A vector of thread descriptors is used to store the result.

# initialize the shared library
interp:library "afnix-sys"
# shared vector of threads descriptors
const thr-group (Vector)
# waits until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))

The group of threads is represented as a vector. Based on the previous discussion, a simple loop that blocks until all threads are completed is designed as a simple gamma expression.

# initializes a matrix with random numbers
const init-matrix (n) {
  trans i (Integer 0)
  const m (Vector)
  do {
    trans v (m:add (Vector))
    trans j (Integer)
    do {
      v:add (afnix:sys:get-random)
    } (< (j:++) n)
  } (< (i:++) n)
  eval m
}

The matrix is literally just a vector of lines, and every line is its own vector of random integers. Simple. (Worth knowing tho: the standard math module ships a native implementation of real matrix, so you don't have to hand-roll this. But hand-rolling is the vibe here.)

# this procedure multiply one line with one column
const mult-line-column (u v) {
  assert (u:length) (v:length)
  trans result 0
  for (x y) (u v) (result:+= (* x y))
  eval result
}
# this procedure multiply two vectors assuming one
# is a line and one is a column from the matrix
const mult-matrix (mx my) {
  for (lv) (mx) {
    assert true (vector-p lv)
    for (cv) (my) {
      assert true (vector-p cv)
      thr-group:add (launch (mult-line-column lv cv))
    }
  }
}

OK so this is the main character of the whole example: matrix vector multiplication. Every single line-column multiplication gets yeeted into its own thread, and that thread object gets parked into the thread group vector. That's the whole trick, no cap.

# check for some arguments
# note the use of errorln method
if (== 0 (interp:argv:length)) {
  errorln "usage: axi 0607.als size"
  afnix:sys:exit 1
}
# get the integer and multiply
const n (Integer (interp:argv:get 0))
mult-matrix (init-matrix n) (init-matrix n)
# wait for all threads to complete
wait-all
# make sure we have the right number
assert (* n n) (thr-group:length)

Execution kicks off with the matrix size as the first argument. Then two random matrices get created and the multi-threaded multiplication is launched. The main thread just sits there blocked until every thread in the thread group is done. Periodt.

Condition variable πŸ’€

A condition variable is another way to sync up your threads. It's modeled with the Condvar object. When you construct it, it starts out initialized to false. Any thread that calls the wait method will block until the condition becomes true. Then the mark method is what a thread uses to flip the state of a condition variable and eventually wake up the threads that are blocked on it. Here's the glow-up:

# create a condition variable
const cv (Condvar)
# this function runs in a thread - does some
# computation and mark the condition variable
const do-something nil {
  # do some computation
  ....
  # mark the condition
  cv:mark
}
# start some computation in a thread
launch (do-something)
# block until the condition is changed
cv:wait-unlock
# continue here

So the condition variable gets created right at the beginning, the thread starts, and the main thread blocks until that thread changes the state of the condition variable. Important detail alert: notice it's wait-unlock being used. When the main thread restarts (after the condition variable has been marked), the main thread owns the lock associated with the condition variable. wait-unlock unlocks that lock when the main thread restarts. Also, wait-unlock resets the condition variable. If you'd used wait instead of wait-unlock, the lock would still be owned by the main thread, and any other thread trying to call mark would just block until that lock gets released. πŸ’€

The Condvar class has a bunch of methods for controlling the behavior of the condition variable, mostly lock control. reset resets the condition variable. lock and unlock control the condition variable locking. And mark, wait and wait-unlock control the synchronization among all the threads.

Function expression ✨

A lambda expression or a gamma expression is basically a function object with no name. During evaluation, the expression object gets evaluated, then the arguments β€” from left to right β€” and a result pops out by applying those arguments to the function object. An expression can even be built dynamically as part of the evaluation process. Iconic.

(axi) println ((lambda (n) (+n 1)) 1)
2

The only difference between a lambda expression and a gamma expression is the nameset binding during evaluation. The lambda expression's nameset is linked with the calling one, while the gamma expression's nameset is linked with the top level nameset. Gamma expressions are especially interesting with recursive functions, since they can generate a significant execution speedup. The example above behaves exactly the same with a gamma expression:

(axi) println ((gamma (n) (+n 1)) 1)
2

Self reference πŸͺž

When you combine a function expression with recursion, you hit a problem: the function needs to call itself, but the function expression doesn't have a name. So the writing system gives you the reserved keyword self, which is a reference to the function expression. Classic factorial, written in pure functional style:

(axi) println ((gamma (n)
    (if (<= n 1) 1 (* n (self (- n 1))))) 5)
120

Choosing a gamma expression over a lambda expression is honestly just a matter of speed. Since the gamma expression has no free variables, symbol resolution isn't a concern here. Slay.

Closed variables πŸ”’

One of the writing system's characteristics is how it treats free variables. A variable is free if it's not bound in the expression environment or its children at the time of symbol resolution. So like, the expression ((lambda (n) (+ n x)) 1) computes the sum of the argument n with the free variable x. Evaluation succeeds if x is defined in one of the parent environments.

Actually this example also shows off the lambda vs gamma difference. Look:

trans x 1
const do-print nil {
  trans x 2
  println ((lambda (n) (+ n x)) 1)
}

The gamma expression do-print will produce 3 since it sums the argument n bound to 1 with the free variable x, which is defined in the calling environment as 2. Now rewrite that same example with a gamma expression and the result is 1, because the expression's parent is the top level environment that defines x as 1.

trans x 1
const do-print nil {
  trans x 2
  println ((gamma (n) (+ n x)) 1)
}

So yeah, clearly there's a need to determine a particular symbol value during expression construction. That's called closing a variable. Closing a variable is a mechanism that binds a particular symbol with a value into the expression, and that symbol is called a closed variable, since its value is closed under the current environment evaluation. The previous example rewritten to close the symbol x:

trans x 1
const do-print nil {
  trans x 2
  println ((gamma (n) (x) (+ n x)) 1)
}

Note that the list of closed variables comes immediately after the argument list. In this case, the function do-print will print 3, since x has been closed with the value 2 as defined in the function do-print.

Dynamic binding πŸŒ€

Because there's a dynamic binding symbol resolution, it's possible under some circumstances to end up with a free or closed variable. This kind of situation happens when a particular symbol is defined under a condition.

lambda (n) {
  if (<= n 1) (trans x 1)
  println (+ n x)
}

Here, the symbol x is a free variable if the argument n is greater than 1. This mechanism can be powerful, but extreme caution should be made when using such feature. Like, fr fr, be careful.

Lexical and qualified names 🏷️

The basic forms elements are the lexical and qualified names, and they're constructed by the parser. Even though the evaluation process makes that lexical object transparent, you can still manipulate them directly.

(axi) const sym (protect lex)
(axi) println   (sym:repr)
Lexical

Here the protect reserved keyword is used to avoid evaluating the lexical object named lex, so the symbol sym refers to a lexical object. Since a lexical β€” and a qualified β€” object is also a literal object, the println reserved function works fine and prints the object name. In fact, a literal object provides the to-string method that returns the string representation of a literal object.

(axi) const sym (protect lex)
(axi) println   (sym:to-string)
lex

Symbol and argument access πŸ”

Every nameset keeps a table of symbols. A symbol is a binding between a name and an object, and eventually the symbol carries the const flag. During lexical evaluation, the lexical object tries to find an object in the nameset hierarchy β€” and that object can be either a symbol or an argument. Again, this process is transparent, but you can control it manually. Both lexical and qualified named objects have the map method, which returns the first object associated in the nameset hierarchy.

(axi) const obj 0
(axi) const lex (protect obj)
(axi) const sym (lex:map)
(axi) println   (sym:repr)
Symbol

A symbol is also a literal object, so to-string and to-literal return the symbol name. There are symbol methods for accessing or modifying symbol values, and you can even change the const symbol flag with the set-const method.

(axi) println (sym:get-const)
true
(axi) println (sym:get-object)
0
(axi) sym:set-object true
(axi) println (sym:get-object)
true

A symbol name cannot be modified, because the name has to stay synchronized with the nameset association. On the other hand, a symbol can be explicitly constructed. And like any object, the = operator assigns a symbol value β€” it behaves like the set-object method.

(axi) const sym (Symbol "symbol")
(axi) println sym
symbol
(axi) sym:= 0
(axi) println (eval sym)
0

Closure πŸ“¦

As an object, the Closure can be manipulated outside the traditional declarative way. A closure is a special object that holds an argument list, a set of closed variables, and a form to execute. The mechanics of closure evaluation were described earlier. What we care about here is being able to manipulate a closure as an object and eventually modify it. Note that by default a closure is constructed as a lambda expression. With a boolean argument set to true, you get the same result. With false, a gamma expression is created.

(axi) const f (Closure)
(axi) println (closure-p f)
true

ok so real quick β€” this next bit is literally the closure glow-up section and it's lowkey my fave. Let's go πŸ’…

This example creates an empty closure. The default closure is equivalent to trans f nil nil. The same thing can be obtained with const f (Closure true). And for a gamma expression, these two forms are equivalent: const f (Closure false) and const f nil nil. Remember that it's trans and const that differentiate between a lambda and a gamma expression β€” that's the whole vibe check right there. Once the closure object is defined, the set-form method can be used to bind a form. πŸŽ€

# the simple way
trans f nil (println "hello world")
# the complex way
const f    (Closure)
f:set-form (protect (println "hello world"))

🌸 Muting a Closure (the part where it gets spicy) 🌸

There are numerous situations where it is desirable to mute dynamically a closure expression, bestie. The simplest one is the closure that mutes itself based on some context β€” with the use of self, a new form can be set to the one that is executed. ✨

Another use is a mechanism call advice, where some new computation are inserted prior the closure execution.

⚠️ Note that appending to a closure can lead to some strange results if the existing closure expression uses return special forms. Like, not "quirky" strange, actually strange. You've been warned 🫢

In a multi-threaded environment, the ability to change a closure expression is particularly handy. For example, a special thread could be used to monitor some context. When a particular situation develops, that threads might trigger some closure expression changes. πŸ‘‘

πŸ’€ Note that changing a closure expression does not affect the one that is executed. If such change occurs during a recursive call, that change is seen only at the next call. Periodt.

and that's the whole page, bestie!! 🌈 You just read the AFNIX Users Manual from AFNIX, page STD-US-GUIDE(7), and honestly?? iconic behavior. Go write some closures, you absolute main character. πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from