diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac8c9e2 --- /dev/null +++ b/README.md @@ -0,0 +1,2366 @@ +# Simple Music Notation Processor + +## What is it? +SMNP is a command line tool aimed on doing music stuff using custom domain-specific language. +You are able to create music (both monophonic and polyphonic) using simple notation +and then synthesize it with custom settings, plot the signal, evaluate DFT on it and so on. +Music tools can be used not only to create music but also to prepare kind of scenarios +of ear trainings like recognizing intervals etc. +Apart of that developed domain-specific language offers you tools known from most popular +programming languages like conditional statements, loops, variable mechanism, +functions (including defining custom ones) etc. + + +## For what? +You might ask whether such kind of tool including design of custom language isn't +over-engineering of ear-training problem. There are a lot of ear-training tools developed even +on mobile platforms which can make them more convenient to use because of their portability feature. + +The reasons for this tool are: +* I'm Java developer and I just wanted to get know more Python, which I used to use at the college. +And well, I'm starting from the assumption, that te best thing that you can do to learn new programming +language is... creating a new one with it. ;-) +Besides, I'm kind of interested in technical side of programming languages that I'm using at work, +so designing and implementing a custom language from scratch would be a nice experience. +* I'm musician also and no one of available tools is suitable for me. +I'm church organist and most of my work is based on dialogue between me and priest. He can +sing melodies in different keys and it requires me to answer keeping the same key. +My tool allows me to create scenarios that can pick one key randomly and play melody +waiting for my answer (basing on input from microphone). +* As a musician I'm also keen on physic nature of sounds and relations between them. +All audio stuff (except hardware layer) implemented to this tool is created from scratch. +It means that the tool synthesises sound by itself and makes use of 3rd party library +only to send compiled wave to speakers. + +## Disclaimers +1. Readability of the code and its structure is one of most important things +related to educational aspect of the project. And in spite of having huge negative impact +on efficiency of the tool, according to one of the assumptions +it has much higher priority. So don't be surprised +if the tool turns out to be extremely slow or ineffective. +2. I try writing consistent code and sticking with some convention of programming of course, +however, as a Java developer I like Java guidelines and I really don't +like PEP8 (especially snakecased identifiers). +Just don't be surprised if you see Java-like code - it has been intentionally written. + +## SMNP language +As mentioned before, SMNP introduces new language. All language evaluation engine (tokenizer, parser and evaluator) +is implemented without any 3rd party libraries and frameworks, just using vanilla Python. + +### About language +SMNP language is interpreted, high-level, formal language with syntax +mostly inspired by Java language and - according to Chomsky's hierarchy - *context-free* grammar. + +#### Type system +SMNP is dynamic language, because there is no any static analyser between +parsing and evaluating stages. It means that any type-mismatching +will be raised on runtime, when control flow reacheas the point of error. + +For example: +``` +function foo(integer n) { + println(n); +} + +x = false; + +if (x) { + foo("hello"); +} +``` +As long as `x == false`, the code will be executed without any errors, +even though `foo` function expects `integer` argument and is being called +with `string`. +However if you switch `x` value to `true`, error will be raised when +control flow reaches `foo` function invocation with wrong argument's type. + +Even though there is no real definition of *strongly-typed language*, +we can say SMNP is strongly-typed, because there are no any implicit type +conversions. You always have to provide correct type, even for +string concatenation which accepts only `string` values: +``` +# Incorrect +"My number is: " + 14; + +# Correct +"My number is: " + 14.toString(); +``` + +### Comments +SMNP language allows you to make comments in the code. +It can be done with `#` character, like: +``` +# This is is a comment +``` +There is no syntax for multiline comment, but you can of course do something like this: +``` +# This is +# a multiline +# comment +``` +Note that because of hash-beginning comments you can +put a *shebang* at the very first of your code making it more +convenient to execute: +``` +#!/usr/bin/smnp +println("Hello, world!"); + +# And now add executable flag (chmod +x) +# to the file and execute it like any other +# script/program from the shell +``` + +### Delimiter +SMNP language doesn't require you to delimit instructions, however it is still possible +and highly recommended, because it helps you to get rid of code ambiguity. + +Example: +``` +size = [1, 2, 3].size +(size - 1) as i ^ print(i) +``` +Execution of this code is interrupted with error, because SMNP parser +tries to interpret `size` property as method `size(size - 1)`. +As long as lists don't have `size` method (but they have `size` property), +error will be raised and you will be able to fix problem. However ambiguity could be +a less obvious and you can stick with debugging code having no idea what is going wrong. + +To remove ambiguity you can end each instruction with semicolon `;`: +``` +size = [1, 2, 3].size; +(size - 1) as i ^ print(i); # 01 +``` + +All code snippets of present document follows the convention of ending each +instruction with semicolon. + +### Basic types +SMNP language introduces 9 data types and *void* which is a *quasi-type* because of it special meaning. +Most of the types is known from other popular programming languages, like Java etc. + +#### integer +`integer` is numeric data type and is suitable to signed integers, like -6, 0, 1, 14, 23164 etc. + +Example: +``` +a = 1; +b = -15; + +sum = a + b; # Produces also an integer +println(sum); # -14 +``` + +###### Methods: +* `toString()` [string] - returns a string representation of integer: `14.toString() == "14"` + +###### Unary operators: +* `-` - negates value + +###### Binary operators: +* `+` - sum +* `-` - difference +* `*` - product +* `/` - quotient +* `**` - power +* `==` - equals +* `!=` - not equals +* `>` - greater than +* `>=` - greater or equals +* `<` - less than +* `<=` - less or equal + +#### float +`float` is numeric data type suitable for non-integer values, like: -3.4, 0.01, 3.14, 12.0043 etc. + +Example: +``` +pi = 3.14; +r = 12.5; + +area = pi*r**2; +println(area); # 490.625 +``` + +###### Methods: +* `toString()` [string] - returns a string representation of float: `1.4.toString() == "1.4"` + +###### Unary operators: +see `integer` + +###### Binary operators: +see `integer` + +#### string +`string` type is suitable to any sort of texts enabling you +to create string of any characters, including Unicode. +For now, strings can be delimited only with double quote (`"`). + +Example: +``` +text = "Hello, world!"; + +println(text); # Hello, world! +println(-text); # !dlrow ,olleH +``` + +###### Properties +* `length` [integer] - length of string: `"hello".length == 5` + +###### Methods +* `join(list l)` [string] - join all elements of list `l` with +given string as delimiter: `":".join(["1", "2", "3", "4"]) == "1:2:3:4"` +* `toString()` [string] - returns itself ;-) + +###### Unary operators +* `-` - reverse string: `-"Hey!" == "!yeH"` (why not? ;-)) + +###### Binary operators +* `+` - concatenate strings: `"he" + "llo" == "hello"` +* `==` - equals +* `!=` - not equals + +#### bool +`bool` data type allows you to perform basic Boolean logic +operations introducing two constant values: `true` and `false`. + +Example: +``` +_2b = false; + +println(_2b or not _2b); # true +``` + +###### Methods: +* `toString()` [string] - returns a string representation of bool: `true.toString() == "true"` + +###### Unary operators +* `not` - negate value: `not false == true` + +###### Binary operators +* `and` - logical conjunction: `true and false == false` +* `or` - logical alternative: `true or false == true` +* `==` - equals +* `!=` - not equals + +#### note +`note` is basic data type allowing you to compose music sheets. +It is a base music unit that represents a sound, i.e. its pitch and duration. + +Note literal is written with the following syntax: +``` +'@' PITCH [OCTAVE] [:DURATION [d]] + +; where + +PITCH := (c|d|e|f|g|a) [b|#] | h# | b + | (C|D|E|F|G|A) [B|#] | H# | B +OCTAVE := 1-9 +DURATION := /non-negative integer/ +``` +Duration number means the denominator (`n`) of fraction `1/n`, i.e.: +* `1` stands for whole note +* `2` stands for half note +* `4` stands for quarter note +* `8` stands for eighth note +* `16` stands for sixteenth note +* and so on + +You can also put a `d` character after duration number to add a ***d**ot* to note, like: +* `2d` stands for half note and quarter note (dotted half note) +* `4d` stands for quarter note and eighth note (dotted quarter note) +* `16d` stands for sixteenth note and thirty-second note (dotted sixteenth note) +* and so on + +Default octave is `4` (1 Line) and default duration is `4` (quarter note). +Examples (note that pitch is case-insensitive): +* `@c` is quarter note with pitch *c'* +* `@F5:2` is half note with pitch *f''* +* `@g#3:4d` is dotted quarter note with pitch *g♯* +* `@Ab6:16` is sixteenth note with pitch *a♭'''* +* `@b2:1` is whole note with pitch *B* (*H♯*) +* `@C#1:32d` is dotted thirty-second note with pitch *C♯,* + +**Note:** note literal syntax cannot include any whitespace character. + +###### Properties +* `pitch` [string] - pitch of note: `@d#5:2d.pitch == "DIS"` +* `octave` [integer] - octave of note: `@d#5:2d.octave == 5` +* `duration` [integer] - duration of note: `@d#5:2d.duration == 2` +* `dot` [bool] - does note is dotted: `@d#5:2d.dot == true` + +###### Methods +* `withOctave(integer octave)` [note] - factory method that copies +note with new `octave` value: `@c.withOctave(5) == @c5` +* `withDuration(integer duration)` [note] - factory method that copies +note with new `duration` value: `@c.withDuration(2) == @c:2` +* `withDot(bool dot)` [note] - factory method that copies +note with new `dot` value: `@c.withDot(true) == @c:4d` +* `toIntRepr()` [integer] - convert note's pitch and octave to unique integer value +* `transpose(integer semitones)` [note] - copy note and transpose +it with given number of semitones: `@c.transpose(2) == @d` +* `toString()` [string] - returns a string representation of note: `@g#.toString() == "G#"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### sound +`sound` is a wrapper for external music file, like `ogg`, `mp3` etc. +This is the only type that isn't possible to create syntactically. +Instead of that it can be instantiated with constructor-like function, +as at the example below: +``` +myMusic = Sound("Music/Piano/Chopin/NocturneOp9No2.ogg"); +myMusic.play(); +``` + +###### Properties +* `file` [string] - a sound source file +* `fs` [integer] - sampling rate + +###### Methods +* `play()` [void] - play a loaded sound file +* `toString()` [string] - returns a string representation of sound: `Sound("/../../music.ogg").toString == "/../../music.ogg"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### list +`list` is an ordered container able to store objects with different types. +List is created within square brackets (`[` and `]`) with items separated by comma (`,`). +Lists can be nested, which means they can contain another +lists that can contain yet another lists and so on. + +Example: +``` +myList = [1, "hello", @Ab:2d, true, 14.0, [ "even", "other", list!"], [], {}]; +println(myList.size); # 9 +println([14].size); # 1 +println([].size); # 0 +println(myList.contains(1)); # true +println(myList.contains(2)); # false +``` + +###### Properties +* `size` [integer] - a number of elements in list + +###### Methods +* `get(integer index`) [?] - returns item of list with given index (**note:** indices start from **0**) +* `contains(element)` [bool] - test if list does contain given element +* `toString()` [string] - returns a string representation of list: `[1, 2].toString() == "[1, 2]` + +###### Unary operators +* `-` - reverse lists (just because!): `-[1, 2, 3, 4] == [4, 3, 2, 1]` + +###### Binary operators +* `+` - join lists: `[1, 2] + [3, 4] == [1, 2, 3, 4]` +* `==` - equals +* `!=` - not equals + + +#### map +`map` is unordered container able to store pairs `key-value` with different +types of both key and value. +Syntactically map is a set of pairs `key-value` separated with comma (`,`) and placed +between braces (`{` and `}`). Single `key-value` pair is created from two items +separated with arrow operator `->`. Similarly to lists, maps can also be nested. +Keys of course must be unique for single map object. + +Even though value of pair can be arbitrary expression, key +should be explicit literal, like integer, note, bool value, type and string. +Lists and other maps can't be used as keys. Note that if string +key doesn't have any whitespaces, there is no need to use quotes around it. + +Example: +``` +myMap = { + 1 -> "hello", + @c -> "world", + true -> false, + "hey" -> 14, + hey2 -> "key without quotes!", + empty -> {}, + theList -> [1, 2, [], { inside -> ":-)" }] +}; + +println(myMap.size); # 4 +println(myMap.get(@c)); # world +println(myMap.get("hey")); # 14 +println(myMap.get("hey2")); # key without quotes! +``` + +###### Properties +* `size` [integer] - number of map entries (pairs) +* `keys` [list] - list of keys: `{ a -> 1, b -> 2 }.keys == ["a", "b"]` +* `values` [list] - list of values: `{ a -> 1, b -> 2}.values == [1, 2]` + +###### Methods +* `containsKey(key)` [bool] - test if map does contain pair with given key +* `containsValue(value)` [bool] - test if map does contain pair with given value +* `contains(key, value)` [bool] - test if map does contain pair with given key **and** given value +* `toString()` [string] - returns a string representation of map: `{ a -> 1, b -> 2}.toString() == "{'a' -> '1', 'b' -> '2'}"` + +###### Binary operators +* `+` - join maps: `{ a -> 1 } + { b -> 2 } == { a -> 1, b -> 2 }` +* `==` - equals +* `!=` - not equals + +#### type +`type` represents all available data types including itself. +It is mostly used to get meta information about other values in code. + +Example: +``` +myNumber = 14; +println(typeOf(myNumber) == integer); # true +println(typeOf(myNumber) == bool); # false +println(typeOf(myNumber) == note); # false +println(typeOf(typeOf(myNumber)) == type); # true +println(typeOf(type)); # type +``` + +###### Methods +* `toString()` [string] - returns a string representation of type: `integer.toString() == "integer"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### void +`void` is special data type introduced to distinguish functions returning a value from +functions that don't return anything. It can't be used in any way and the only possibility +to do something with that is getting the type of it or getting a string representation of it. + +Example: +``` +void; # It actually does nothing +println(void.toString()); # void +println(typeOf(void)); # type +``` + +### Variables +SMNP language's variable mechanism allows you to store some data in memory which could be +used in the later stages of code. Even though SMNP language has a data types, declaring +new variables doesn't require you to explicitly put a type before declaration as in the case +of most popular languages, like Java etc. Hence it is not possible to declare variable +without initialization. You always have to initialize your variable when you are declaring it. +Initialization can be done with assignment operator (`=`). The right hand side should be +expression (value, function call or even other variable) but the left hand side can be only +variable identifier. + +``` +myVar; # It is not a declaration +myVar = 2; # It is a declaration. From now you are able to use variable myVar +3 = 2; # error! +var2 = foo(); # Correct +var2 = myVar; # Also correct +``` + +Variables are nothing but references to objects (values) and their don't have any type. +In other words you are able to assign value of different type to already initialized variable. + +``` +myVar = 2; +println(myVar); # 2 +myVar = "Hello, world!"; +println(myVar); # Hello, world! +``` + +Because assignment is an expression which returns a value being assigned it is possible +to perform multiple initialization. + +``` +a = b = c = 10; # It is like a = (b = (c = 10)) +println(a == b and b == c); # true +``` + +SMNP language doesn't have any equivalent of Python's `del` instructions, so you aren't +able to delete already created variable. The only way is using scopes. + +#### Scopes +Scopes are strongly related to blocks. +Block is a set of statements and expressions bounded on both sides with `{` and respectively `}`. +Even though it is not required and you can write whole code in one line, it is highly recommend +to use any kind of indentations for each block, including blocks nested inside. +``` +println("I'm outside any block"); + +{ + println("I'm in the first-level block"); + { + println("I'm in second-level block"); + } + + println("Greetings from first-level block again"); +} + +``` + +In context of variable mechanism it is important to know that variables declared inside +block are available to each instructions following declaration **in the same block** +and for each nested block placed after variable declaration. + +``` +var1 = "top-level"; +{ + var2 = "first-level"; + println(var1); # "top-level" + println(var3); # error! + { + var3 = "second-level"; + println(var1); # "top-level" + println(var2); # "first-level" + println(var3); # "second-level" + } + println(var2); # "first-level" + println(var3); # error! +} + +println(var2); # error! +println(var1); # "top-level" +``` + +#### Identifiers +It is also important to know what is identifier in context of SMNP language. +Identifier is a string that contains only letters (both lowercase and uppercase), numbers and `_` character. +Identifier must not start with number and must not be any of reserved words (keywords). + +List of keywords: +* `and` +* `or` +* `not` +* `integer` +* `string` +* `float` +* `note` +* `bool` +* `type` +* `list` +* `map` +* `function` +* `return` +* `extend` +* `import` +* `throw` +* `from` +* `with` +* `if` +* `else` +* `as` + + +``` +# Valid identifiers +i +var +myVar +my_var +var20 +x_YZ +_vArIaBlE +_if +_return +returns + +# Invalid identifiers +19i +foo[ +bar@ +return +!@#%$ +if +as +``` + +#### Immutability +It's a good place to say that all values in SMNP code are **immutable**. +That means you are not able to e.g. change pitch of existing note, change arbitrary letter +of string, put object to list and so on. + +You are able to produce note basing on given one with new pitch or join two or more lists +but these operations produces new objects and leave values unmodified. + +``` +a = [1, 2]; +b = [3, 4]; +c = a + b; +println(a == [1, 2]); # true +println(b == [3, 4]); # true +println(c == [1, 2, 3, 4]); # true + +a = @c; +b = a.withDuration(2); +println(a == @c); # true +println(b == @c:2); # true +``` + +### Operators precedence +SMNLP language's operators have their unique priorities which determine operations' order +in case of ambiguity. + +| Operator(s) | Precedence | +|----------------:|:----------:| +| `-` (unary) | 1 | +| `.` | 2 | +| `**` | 3 | +| `not` | 4 | +| `*` `/` | 5 | +| `+` `-` (binary)| 6 | +| `==` `!=` | | +| `<=` `>=` | 7 | +| `<` `>` | | +| `and` | 8 | +| `or` | 9 | +| `^` | 10 | + +Remember, that in spite of operators precedence you can always force priority using parentheses: +``` +a = -2+2; +b = -(2+2); + +println(a); # 0 +println(b); # -4 +``` + +### Methods and functions +Function is a code snippet that takes some arguments and produces result. +Both arguments and result can be optionals. +Method in turn is a special function that is called in behalf of some +object. +Function (and method of course) can be invoked using its name and optional arguments +separated with commas (`,`) bounded on both sides with parentheses (`(` and `)`). +Even if no arguments are passed you have to put empty arguments list: `()`. + +Some functions are available out-of-the-box as they are implemented +to SMNP tool. Set of such functions is called *standard library* (you can +read more about standard library in later section). You are able also +to create custom functions and methods (it'll be covered in next section). + +As you can see on below examples implemented functions and methods mechanism +is really similar to other popular languages, like Java or Python. + +Examples: +``` +# Invoking function 'println' without any arguments +println(); + +# Invoking function 'println' with 1 argument +println("Hello, world!"); # Hello, world + +# Invoking function 'println' with 2 argument +println(1, 2); # 12 + +# Invoking function 'println' with 2 argument +# Invoked function produces also result which +# is being assigned to variable 'newNote1' +newNote1 = transpose(2, @c); + +# Method equivalent of function above +newNote2 = @c.transpose(2); + +# Another example of method +firstElementOfList = [14, 3, 20, -4].get(0); + + +println(newNote1 == newNote2); # true +``` + +Note, that some functions or methods don't return anything. +In this case expecting any returned value can throw an exception. +``` +# 'println' is example of function, that doesn't return anything +# in this case following instruction will raise an error +println(println()); # error + +# other examples +x = println(); # error + +println([1, 2, 3, println(), 5, 6]); # error +``` + +#### Function/method signature +Signature is feature of both functions and methods that makes them unique. +Signature consists of function/method name, function/method arguments +and applicable type in case of method. +It is a good place to say, that methods technically are just simple functions +with one additional argument placed at the beginning of arguments list. +Because of that we can say that signature consists only of name and arguments +list. This is also the reason of a little bit meaningless errors +that you can get when you are trying to call method with invalid arguments' types: +``` +# correct +x = [1, 2, 3].get(0); + +# invalid +x = [1, 2, 3].get(@c); +``` +in this case you'll get following error (note first argument type +of expected signatures): +``` +Invocation Error +(...) + +Expected signature: +get(list, integer) +or +get(map, ) + +Found: +get(list, note) +``` + +### Custom methods and functions +SMNP language introduces possibility to define custom functions and methods. +They can be invoked later in the same way as regular functions/methods +provided by SMNP standard library. + +Functions and methods can be defined only at top level of the code, which means +they cannot be nested in any blocks. + +#### Function definition +Functions can be defined with `function` keyword, like it is shown on following example: +``` +function multipleBy2(number) { + return 2*number; +} + +# Correct invocations +x = multipleBy2(2); # x = 4 +x = multipleBy2(14); # x = 28 +x = multipleBy2("hey"); # in spite of correctness it will cause an error + # because string argument doesn't support '*' operator + +# Incorrect invocations +x = multipleBy2(1, 2); # it'll cause Invocation Error because of signatures mismatch +x = multipleBy2(); # as above +``` +Thanks to `return` keyword you can produce an output value basing on e.g. passed arguments. +Example above takes one arbitrary argument, multiplies it by 2 and then returns a result. + +##### Explicit argument types +Because arbitrary argument can be passed, the `multipleBy2` function can throw an error +in case of trying to multiple e.g. note or string, that don't support `*` operator. +In this case you can put a constraint to the argument which accepts only chosen types: +``` +function multipleBy2(integer number) { + return 2*number; +} + +# Correct invocations +x = multipleBy2(2); # x = 4 +x = multipleBy2(14); # x = 28 + +# Incorrect invocations +x = multipleBy2(1, 2); # it'll cause Invocation Error because of signatures mismatch +x = multipleBy2(); # as above +x = multipleBy2("hey"); # as above +``` +Function `multipleBy2` will work only with integer argument now. Any attempt to invoke it +with no-integer values will cause Invocation Error related to mismatched signatures. + +Of course you are still able to create functions without any arguments or mix their types: +``` +function noArgs() { + println("Hello, I don't accept any arguments."); + println("Also see that I don't return anything!"); +} + +function mixedArguments(integer a1, note a2, a3) { + println("See, " + a1.toString() + " is an integer!"); + println("And " + a2.toString() + " is a note."); + println("Type of third argument is " + typeOf(a3).toString()); +} + +# Correct invocations +mixedArgument(1, @c, "hey"); +mixedArguments(14, @Gb:4d, true); + +# Incorrect invocations +mixedArgument(@c, @c, @c); +mixedArgument(1, 1, 1); + +``` +##### Functions returning nothing +Notice also that functions presented in previous example don'tactually return anything. +Technically they **do** return a `void` type and it is transparent to SMNP user. You should remember, that +you can expect value produced by function only and only if `return` statement +is declared in invoked function and the statement **has been called**. Otherwise +function being invoked will return `void` type which cannot act as expression. + +##### Ambiguous arguments +SMNP language allows you also to declare more than one argument that functions can accept. +Accepted types are separated with commas (`,`) and are bounded on both sides with `<` and `>` characters. + +Example: +``` +function foo( x, sound y, z) { + # 'x' can be either string, bool, integer or float + # whereas 'y' can be only a sound + # and 'z' can be of any available type +} + +# Correct invocations +foo("hey", Sound("..."), [1, 2, true, [@c], { a -> 1, b -> 2 }]); +foo(true, Sound("..."), integer); +foo(14, Sound("..."), @c#:16d); +foo(1.4, Sound("..."), 3.14); + +# Incorrect invocations +foo(integer, Sound("..."), 10); +foo("hey", 20, 10); +foo([1, 2, 3], Sound("..."), 10); +``` + +##### Specified lists +SMNP language allows you to specify what objects can be contained in lists. +It can be done with construction similar to previous one. +Accepted types are separated with commas (`,`), bounded on both sides with `<` and `>` characters and placed +next to `list` keyword: +``` +function foo(list x) { + # 'x' can be only list of integers +} + +# Correct invocations +foo([1, -2, 3]); +foo([0]); +foo([]); + +# Incorrect invocations +foo(1); +foo(2, 3); +foo([ 1, 2, @c ]); +``` + +You are still able to use multiple type as constraints: +``` +function foo(list x) { + # 'x' can be only list of integers and notes, nothing else +} + +# Correct invocations +foo([1, @c, 4]); +foo([]); +foo([0]); +foo([@d#]); + +# Incorrect invocations +foo([1, @c, true]); +foo(@c, 1); +foo({ @c -> 1 }); +``` + +List specifier can be empty or even be absent. In this case list can contain +any available types: +``` +function foo(list<> x) { + # 'x' can be only a list containing arbitrary objects +} + +# the same function can be implemented as + +function foo(list x) { + # ... +} + +# Correct invocations +foo([1, @c, true]); +foo([]); +foo([integer, float, @g#:32d, false, [[[]]], { a -> 1, @d -> 2, true -> {} }]); + +# Incorrect invocations +foo({}); +foo([1, @c, true], 1); +foo(true); +``` + +Specifiers can be nested - you are able to create e.g. list of lists of lists of integers etc. +``` +function foo(list>> x) { +} + +# Correct invocations +foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]); + +# Incorrect invocations +foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 9]); +``` + +Of course ambiguous arguments can include specified lists: +``` +function foo( x) { + # 'x' can be integer, note or list containing only integers and notes +} + +# Correct invocations +foo(@c); +foo(1); +foo([]); +foo([2, @G]); +foo([@h]); + +# Incorrect invocations +foo(1.0); +foo([true]); +``` + +##### Specified maps +Maps can be also specified. All rules related to specified lists are also applicable for maps. +The only difference is map supports **2** specifiers: first one for keys and second one for values. + +Example: +``` +function foo(map x) { + # 'x' can be only a map with strings as keys and notes as values +} + +function bar(map<> x) { + # 'x' can be only a map with strings as keys and arbitrary values +} + +function xyz(map x) { + # 'x' can be only a map with strings and booleans as keys and integers and notes as values +} + +function abc(map<> x) { + # 'x' can be only a map with arbitrary keys and integers with booleans as values +} + +# Correct invocations +foo({ c -> @c, d -> @d }); +bar({ a -> @c, b -> 1, c -> true, d -> map, e -> { x -> [] }); +xyz({ a -> 1, true -> @c, false -> 2, b -> @d }); +abc({ a -> true, 1 -> false, @c -> 10, true -> 14 }); + +# Incorrect invocations +foo({ c -> @c, @d -> @d, @c -> "hey" }); +bar({ a -> @c, @d -> @c }); +xyz({ integer -> @c, 2 -> 3, float -> bool }); +abc({ a -> true, false -> @c }); +``` + +##### Optional arguments +SMNP language allows you also to use default arguments. +Optional arguments are special kind of arguments that can have default value and are +totally optional to pass during invocation. Syntax of optional arguments is similar +to variable assignment syntax. + +Example: +``` +function foo(x = 10) { + # 'x' can be of any available type. + # You can override default value but you don't have to. + # In this case default value will be used. + + return x; +} + +# Correct invocations +y = foo(); # y = 10 +y = foo(10); # y = 10 +y = foo(true); # y = true +``` + +All kinds of arguments mentioned so far can be optional: +``` +function foo(x = 1, integer y = 14, > z = [[1, @c], [@d]]) { +} + +# Correct invocations +foo(); +foo(2); +foo(10, 11); +foo(-2, 33, @c); +foo(-2, 33, [[1, @d, @f#], [4, 3], [@c, @d, @e]); +foo(0, 0, []); +foo(0, 0, [[]]); + +# Incorrect invocations +foo(true); +foo(1, 0.5); +foo(10, 2, 13); +foo(0, 0, [], 3); +``` + +As you can see, you can have as many optional arguments as you want. +Just remember, that after first declared optional argument you can't use regular +arguments in the same signature anymore. +``` +# Correct signatures +function foo(a, b, c = 0) {} +function bar(a, b = "hello", c = true) {} +function xyz(a = integer, b = [12.5], c = @c) {} + +# Incorrect signatures (will throw an error) +function abc(a = 0, b) {} +function def(a, b, c = 0, d = true, e, f = @G#) {} +``` +In other words, optional arguments should be declared at the tail of function signature. + +##### Varargs +SMNP language also supports functions' signatures with arbitrary length. +This is exactly how `println` or `print` functions work. +You can pass as many arguments as you want and they still are able to handle it. +SMNP language provides a special operator `...` that enables you to have function +with variable number of arguments. + +Remember that: +* you can have only one vararg at the signature +* vararg should be placed at the very end of signature +* you can't mix varargs with optional arguments in one signature. + +After invocation, all matched arguments are collected into one list which is passed +as vararg. + +Example: +``` +function foo(a, b, c...) { + # 'c' is a list of arguments passed after 'a' and 'b' + + return c; +} + +# Correct invocations: +x = foo(0, 1); # x = [] +x = foo(1, 2, 3, 4); # x = [3, 4] +x = foo(true, false, @c, [3.14, 5, integer], float); # x = [@c, [3.14, 5, integer], float] + +# Incorrect invocations: +foo(); +foo(true); +``` + +Same as optional arguments, vararg can work with any previously mentioned kinds +of arguments (except optional arguments of course). +``` +function foo(a, note b, map> c...) { + # after two arguments you can pass any number of + # maps with strings as keys and list of integers and notes as values +} + +# Correct invocations: +foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> [@e] }); +foo(1, @c, {}, {}, {}, {}, {}, {}, {}, {}); + +# Incorrect invocations: +foo(); +foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> @e }); +``` + +#### Method definition +All rules related to defining custom functions are applicable to methods either. +The difference is method can be invoked in a context of some object. +That's why methods can be defined only as a part of some type. + +##### `extend` statement +SMNP language introduces `extend` statement which allows you to add custom +methods to existing types. All you need is to precise desired type and define function +which will be working as new method. +Following listing presents a syntax of `extend` statement: +``` +extend integer as i with function multipleBy(integer x) { + return i*x; +} + +# From now you can use new method as follows: +x = 10.multipleBy(2); # x = 20 +y = x.multipleBy(4); # y = 80 +``` + +As mentioned before, all rules related to functions are applicable here, so there +is no need to cover `function multipleBy(...) {...}` statement again. + +Code from listing above defines new method of `integer` type. +Note, that thanks to `integer as i` instruction, object that holds the context of method +is available through variable with name `i`. This works similar to arguments +passed to function and actually there are no differences between +using them in function's body. + +Even though type actually works as additional argument passed to function, there +is a slight difference between them. As mentioned before, you can use such features +in function signature as optional arguments, ambiguous arguments, varargs etc. +For obvious reasons these are not available for declaring type. +Type being extended must be declared only as a simple data type or map/list with optional +specifier. Also you have always provide an identifier by which you can refer to the type (`as` part). + +Examples: +``` +# Correct +extend integer as x with function a() {} +extend list as x with function b() {} +extend list<> as x with function c() {} +extend list> as x with function d() {} +extend map as x with function e() {} +extend map<><> as x with function f() {} +extend map<>>>> as x with function g() {} + +# Incorrect +extend as x with function h() {} +extend as x with function i() {} +extend <> as x with function j() {} +extend integer with function k() {} +``` + +Because extending statement supports list/map specifiers, you can define +methods that are applicable only for list/map with specified content: +``` +# Extends all lists, no matter of content +extend list as l with function foo() { + println("foo()"); +} + +# Extends only list +extend list as l with function bar() { + println("bar()"); +} + +l1 = [1, 2, 3, 4]; +l2 = [1, 2, @c, 4]; +l3 = ["a", "b", "c", "d"]; + +l1.foo(); # foo() +l2.foo(); # foo() +l3.foo(); # foo() + +l1.bar(); # bar() +l2.bar(); # error! +l3.bar(); # error! +``` + +Because SMNP doesn't allow you to extend multiple types in one instruction, if you want +extend for example `list` and `list` (but not `list`) you have +to do it separately: +``` +# This won't work at all +extend , list> as l with function foo() { + # ... +} + +# Instead of that you can do: +extend list as l with function foo() { + # ... +} + +extend list as l with function foo() { + # ... +} +``` +Note that there is no signature collision problem, because technically first argument +is different in both methods. + +##### `extend` block +In case of extending type with more than one function you can use block of methods. + +Example: +``` +extend integer { + function multipleBy2() { + return 2*i; + } + + function multipleBy4() { + return 4*i; + } +} + +# Construction above is equivalent of following code: + +extend integer as i with function multipleBy2() { + return 2*i; +} + +extend integer as i with function multipleBy4() { + return 4*i; +} +``` + +#### Overloading function/methods +Unfortunately functions/methods overloading +is not supported in custom functions/methods so far. +However you can notice that some functions and methods coming +from SMNP standard library core are overloaded, like `get()` method, +some constructor-like methods, `wait()` function and so on. +That's because these functions are implemented right in Python +and there still lacks function/method overloading mechanism directly in SMNP language. + +However you can still use ambiguous arguments and just check in function's body +their types by hand using `typeOf()` function: +``` +function myFunction( arg) { + if (typeOf(arg) == integer) { + println("You have passed integer"); + } else { + println("You have passed note!"); + } +} + +myFunction(1); # You have passed integer +myFunction(@c); # You have passed note +myFunction("hey"); # error! +``` + +### Imports +SMNP language allows you to split code into multiple files and then join them together +using `import` statement. +The statement loads file, executes its code and appends newly created context into +main file. It means that all imperative code (code that produces immediately result, like function calls) +will be executed just once, whereas declarative part (code that appends something to context, like +variable assignments or function definitions) will be appended to main context, so that +you will be able to take advantage of defined functions and variables. + +Example: +``` +#################### file_a.mus #################### +function foo() { + println("Hello, world!"); +} + +var = 14; + +println("I'm a file_a.mus!"); +``` +``` +##################### main.mus ##################### +import "file_a.mus"; # I'am a file_a.mus! + +foo(); # Hello, world! +println(var); # 14 +``` + +It is important to know, that `import` is just simple statement that loads external file and executes its code. +Thus there is no any protection from some known dependency problems, like circular dependencies +or repeatedly imported files. + +### `if` and `if-else` statement +SMNP language has support for good old condition statement +and syntax is inspired by Java language. + +#### `if` statement +The simplest form consists only of `if` keyword followed +by condition bounded on both sides with +parentheses (`(` and `)`) and statement +(or block of code): +``` +if (2 > 3) println("It gonna never be displayed!"); + +# or (using block of code) + +if (2 > 3) { + println("It gonna never be displayed!"); +} +``` + +Just like in Java condition **must be** of `bool` type. +``` +# Correct examples +if (true) println(); +if (false) println(); +if (not false) println(); +if (0 == 0) println(); +if ("hello".length > 0) println(); +if (true and not true or not false and true) println(); +if (true and (not true or not false) and true) println(); + +# Incorrect examples +if () println(); +if (0) println(); +if (1) println(); +if ("hello") println(); +if (true.toString()) println(); +``` + +#### `if-else` statement +You can use additional `else` clause which is always executed +when condition is resolved to `false`. + +Example: +``` +if (2 > 3) println("It gonna never be displayed!"); +else println("but this always will be displayed"); + +# or you can use blocks of code to increase readability + +if (2 > 3) { + println("It gonna never be displayed!"); +} else { + println("but this always will be displayed"); +} +``` + +Remember that only one condition statement branch (`if` or `else`) +can be executed in single statement. There is no possibility +to execute both clauses: +``` +x = 1; + +if (x > 0) { + println(true); +} else { + println(false); +} + +if (x < 0) { + println(false); +} else { + println(true); +} +``` +Above example will print twice `true`. First statement's condition is met, +so `if` clause did execute, but second statement's condition is resolved +to false so `else` clause did execute. + +#### `if-else-if-else-if-...` statement +Above constructions are sufficient +to create `if-else-if-...` pseudo-construction. +Thanks to it you are able to put additional execution branches +to existing condition statement. +*Pseudo* in this case means that there is no special parsing rules +nor evaluators related to this construction. It is just a combination +of `if` and `if-else` statements presented above. + +Example: +``` +x = 3; +if (x == 0) { + println("x is zero"); +} else if (x == 1) { + println("x is one"); +} else if (x == 2) { + println("x is two"); +} else if (x == 3) { + println("x is three"); +} else { + println("x is neither zero, one, two nor three"); +} + +# technically this construction is: + +if (x == 0) { + println("x is zero"); +} else { + if (x == 1) { + println("x is one"); + } else { + if (x == 2) { + println("x is two"); + } else { + if (x == 3) { + println("x is three"); + } else { + println("x is neither zero, one, two nor three"); + } + } + } +} +``` +Thanks to its tree-like construction control flow falls through all conditions. +A branch first matched branch is executed +and following conditions are not checked anymore: +``` +x = 0 +if (x == 1) { + println(); # Condition is resolved to false +} else if (x == 0) { + println(); # Condition is met +} else if (x == 0) { + println(); # Condition is also met, +} # but it hasn't been even checked because above + # branch has been already matched and executed +``` + +### Loop statement / expression +SMNP language provides a loop statement which also acts as expression. +The statement has been totally designed from scratch and is not similar +to any known loop statements from other languages. +Loop allows you to repeat some instructions with counter set programmatically. +All loops are created through loop operator (`^`). + +#### Simple loop +The simplest loop consists only of counter, loop operator and instruction or block +of instructions. + +Example: +``` +# the simplest loop +3 ^ print("Money "); # Money Money Money + +# the same using block of code + +3 ^ { + print("Money "); +} + +# Output: Money Money Money +``` + +Of course you can use any expression as counter, like variable or function call: +``` +function provideCounter(integer x) { + return x * 2; +} + +provideCounter(5) ^ print("a"); # aaaaaaaaaa +``` + +You can define a counter variable whose value is increased with each iteration +using `as` clause (note, that values start from 0): +``` +3 as i ^ print(i, " "); # 0 1 2 +``` + +#### *while* loop +You can create a loop which is controlled by logic condition. +It can be done just putting a `bool` value as a counter. + +Example: +``` +end = false; +i = 0; + +not end ^ { + if (i == 3) { + end = true; + } + + i = i + 1; +} + +println(end); # true +println(i); # 4 +``` + +Note that condition-controlled loop doesn't support `as` clause. + +#### *For-each* loop +This kind of loop works both with lists and maps. +##### List +You can put a list as a counter. In this case statement will be executed +for each element of list. You can obtain an access to element through `as` clause +like in previous examples. + +Example: +``` +x = [@c, @d, @e]; +x ^ print("Money "); # Money Money Money + +x as n ^ print(n, " "); # C D E +``` + +You can put additional parameter into `as` clause. In this case the first parameter +will be just iterator that is increased after each loop iteration. The second parameter +will be value of passed list. If you are using more than one parameter in `as` clause, +you have to enclose them between parentheses. + +Example: +``` +[@c, @d, @e] as (i, n) ^ println(i, ". ", n); + +# Output: +# 0. C +# 1. D +# 2. E +``` + +##### Map +Map is also supported to act as an counter of loop. +In this case statement will be executed for each value of passed map. + +Example: +``` +x = { + a -> @c, + b -> @d, + c -> @e +}; + +x ^ print("Money "); # Money Money Money +``` + +You are also able to access keys and values of iterated map using parameters in `as` clause. +If you pass only one parameter, it will contain a value of map. +If you pass two parameters, the first one will contain a key and the second one respectively a value. +If you pass three parameters, the first one will be an iterator increased by one with all +iteration, the second one will be key and the third one will be value. + +Example: +``` +myMap = { + first -> true, + second -> [@c, @d, @e], + third -> 14 +}; + +myMap as value ^ println(value); +# Output: +# true +# [C, D, E] +# 14 + +myMap as (key, value) ^ println(key, ": ", value); +# Output: +# first: true, +# second: [C, D, E] +# third: 14 + +myMap as (i, key, value) ^ println(i, ". ", key, ": ", value); +# Output: +# 0. first: true, +# 1. second: [C, D, E] +# 2. third: 14 +``` + +#### Filters +Loops in SMNP language also support filtering. +The filtering can be done with `%` clause, which contains a condition +that must be met to execute iteration. `%` clause is placed at the very last +of loop statement. + +Example: +``` +10 as i ^ println(i) % mod(i, 2) == 0; +# Output: +# 0 +# 2 +# 4 +# 6 +# 8 +``` +Filtering can be useful with simple counting loops like above, but it is really powerful with +*for-each* loops. You can pass further only elements of list/map that met +defined conditions. +It is even more useful with special feature of loops that is covered in next section. + +#### Loop expression +Really strength of designed loops relies on the fact, that all kinds of them actually can work as *expressions*. +It means that you can use any kind of loop as e.g. function argument, because they can produce a value. +They work similar to Python's list comprehension and actually produce a new list. +One important condition you have to met in order to use loop as expression is to put +expression as a loop's statement. Because of that in loop expression you can't use +block of code. The statement must be a single instruction. + +Example: +``` +x = [1, 2, 3, 4]; + +# 'y' holds numbers multipied by 2 +y = x as i ^ i * 2; # y = [2, 4, 6, 8] + +# 'z' holds squares of even numbers +z = x as i ^ i ** 2 % mod(i, 2) == 0; # y = [4.0, 16.0] +``` + +Note, that loop statement is right associative. +That means nested loop statements are being accumulated in right hand branch. +Take look at example: +``` +2 ^ 4 ^ 6 ^ print(); +# is exactly the same as +2 ^ (4 ^ (6 ^ print())); +``` +is parsed to following abstract syntax tree: +``` + ^ + / \ +2 ^ + / \ + 4 ^ + / \ + 6 print("a") +``` +It could be a flaw if you would like to create a mapping chain using loop statements +(like streams in Java 8 or LINQ in .NET), because it requires from operator +to be left associative, so that first instruction would produce value and pass it further. +It can be of course achieved with parentheses: +``` +data = ["lorem", "ipsum", "dolor", "sit", "amet"]; +output = (((((data as d + ^ d + % d.length > 3) as d + ^ d.length) as d + ^ d * 2) as d + ^ d + 1) as d + ^ d + % d == 11); + +println(output); # [11, 11, 11] +``` +As you can see it actually works but is not convenient and readable. + +### Throwing errors +Even though SMNP language doesn't have any sophisticated error handling mechanism, +you are able to raise errors using `throw` statement. +The `throw` construction is inspired by Java language, however instead of +throwing exceptions SMNP language allows you only to throw string values. +When control flow meet `throw` statement, program execution is immediately interrupted +and Execution Error is raised with message passed to `throw` statement. + +Example: +``` +function divide(integer a, integer b) { + if (b == 0) { + throw "You are trying to divide by 0!"; + } + + return a / b; +} +``` +If you try to invoke e.g. `divide(2, 0)` the Execution Error will be thrown +with message, that you are trying to divide by 0. + +SMNP language does not support any equivalent of `try-catch` statements known from +other languages, so you are actually unable to implement more advanced multi-layer +error handling system, because the language is not aimed on building complex systems +and there is simply no need to implement sophisticated error handling system +in simple music . + + +### Standard library +SMNP is provided with builtin standard library which consists of +some fundamental functions and methods that increase language's functionality. +SMNP don't require you to do anything in order to use anything from its standard library. +Just put function/method name with proper arguments and that's it. +Technically SMNP's standard library (*stdlib*) consists of 2 layers: native library and language library. + +#### Native library +Native library contains functions and methods that +are implemented **directly in Python**. +Generally this part of *stdlib* introduces fundamental functions +and methods that: +* enables communication between +SMNP and operating system, like: +`print`, `read`, `exit` etc. +* are related to audio module, like: +`wave`, `synth`, `wait`, `play` etc. +* are strictly bound to runtime environment, like: +`type`, `debug` etc. +* make use of Python libraries, like: +`rand`, `fft`, `plot` etc. +* are constructor-like functions, like: +`Integer`, `Map`, `Note` etc. + +Native functions and methods are located in `smnp/module` module. + +#### Language library +Language library contains functions and methods that are +implemented **in SMNP language itself**. +Some of SMNP assumptions points to move as many code as possible +from native library to language library, even at cost of efficiency. +This library contains such functions and methods as: `flat`, `transpose`, +`mod`, `tuplet`, `sample`, `random`, `join` and so on. + +Language library is a single file named `main.mus` and located +in `smnp/library/code` module. + +#### *stdlib* documentation +Documentation of both native and language library is included in +the `STDLIB.md` file of present repository. + +## SMNP language interpreter +SMNP language interpreter consists of three parts composed +to pipeline: +* *tokenizer* (or *lexer*) +* *parser* +* *evaluator* + +All of these components participate in processing and executing +passed code, producing output that can be consumed by next component. + +### Tokenizer +Tokenizer is the first component in code processing pipeline. +Input code is directly passed to tokenizer which splits +it to several pieces called *tokens*. Each token contains of +main properties, such as *value* and related *token type* (or *tag), +for example: +* literal: `"Hello, world!"` is token with value `Hello, world!` and +token type `STRING` +* literal: `abc123` is token with value `abc123` and token type `IDENTIFIER +* etc. +Apart from mentioned data, each token also includes some metadata, like +location of value in code (both line and column). + +You can check what tokens are produced for arbitrary input code +using `--tokens` flag, for example: +``` +$ smnp --tokens --dry-run -c "[1, 2, 3] as i ^ println(\"Current: \" + i.toString());" +[Current(0): {OPEN_SQUARE, '[', (0, 0)} +{OPEN_SQUARE, '[', (0, 0)}, {INTEGER, '1', (0, 1)}, {COMMA, ',', (0, 2)}, {INTEGER, '2', (0, 4)}, {COMMA, ',', (0, 5)}, {INTEGER, '3', (0, 7)}, {CLOSE_SQUARE, ']', (0, 8)}, {AS, 'as', (0, 10)}, {IDENTIFIER, 'i', (0, 13)}, {DASH, '^', (0, 15)}, {IDENTIFIER, 'println', (0, 17)}, {OPEN_PAREN, '(', (0, 24)}, {STRING, 'Current: ', (0, 25)}, {PLUS, '+', (0, 37)}, {IDENTIFIER, 'i', (0, 39)}, {DOT, '.', (0, 40)}, {IDENTIFIER, 'toString', (0, 41)}, {OPEN_PAREN, '(', (0, 49)}, {CLOSE_PAREN, ')', (0, 50)}, {CLOSE_PAREN, ')', (0, 51)}, {SEMICOLON, ';', (0, 52)}] +``` + +Tokenizer tries to match input with all available patterns, sticking +with rule *first-match*. That means if there is more than one patterns +that match input, only first will be applied. +This is why you can't for example name your variables or functions/methods +with keywords. Take a look at the output of following command: +``` +$ smnp --tokens --dry-run -c "function = 14;" +[Current(0): {FUNCTION, 'function', (0, 0)} +{FUNCTION, 'function', (0, 0)}, {ASSIGN, '=', (0, 9)}, {INTEGER, '14', (0, 11)}, {SEMICOLON, ';', (0, 13)}] +Syntax Error + [line 1, col 10] + +Expected function/method name, found '=' +``` +The first token has type of `FUNCTION`, not `IDENTIFIER` which is expected +for assignment operation. + +All tokenizer-related code is located in `smnp/token` module. + +### Parser LL(1) +Parser is the next stage of code processing pipeline. +It takes input from tokenizer and tries to compose a tree (called +AST, which stands for **a**bstract **s**yntax **t**ree) basing on known rules, which are called *productions*. +As long as tokenizer defines language's *alphabet*, i.e. a set +of available terminals, parser defines *grammar* of that language. +It means that tokenizer can for example detect unknown character +or sequence of characters meanwhile parser is able to detect unknown +constructions built with known tokens. +A good example is last snippet from above section related to tokenizer: +``` +$ smnp --tokens --dry-run -c "function = 14;" +[Current(0): {FUNCTION, 'function', (0, 0)} +{FUNCTION, 'function', (0, 0)}, {ASSIGN, '=', (0, 9)}, {INTEGER, '14', (0, 11)}, {SEMICOLON, ';', (0, 13)}] +Syntax Error + [line 1, col 10] + +Expected function/method name, found '=' +``` +You can see, that tokenizer has successfully done his job, +but parser throw a syntax error saying that it does not know +any production that could (directly or indirectly) match +`FUNCTION ASSIGN INTEGER SEMICOLON` sequence. + +You can check AST produced for arbitrary input code +using `--ast` flag, for example: +``` +$ smnp --ast --dry-run -c "[1, 2, 3] as i ^ println(\"Current: \" + i.toString());" +Program (line 0, col 0) + └─Loop (line 1, col 1) + ├─List (line 1, col 1) + │ ├─IntegerLiteral (line 1, col 2) + │ │ └'1' + │ ├─IntegerLiteral (line 1, col 5) + │ │ └'2' + │ └─IntegerLiteral (line 1, col 8) + │ └'3' + ├─Operator (line 1, col 16) + │ └'^' + ├─FunctionCall (line 1, col 18) + │ ├─Identifier (line 1, col 18) + │ │ └'println' + │ └─ArgumentsList (line 1, col 25) + │ └─Sum (line 1, col 38) + │ ├─StringLiteral (line 1, col 26) + │ │ └'Current: ' + │ ├─Operator (line 1, col 38) + │ │ └'+' + │ └─Access (line 1, col 41) + │ ├─Identifier (line 1, col 40) + │ │ └'i' + │ ├─Operator (line 1, col 41) + │ │ └'.' + │ └─FunctionCall (line 1, col 42) + │ ├─Identifier (line 1, col 42) + │ │ └'toString' + │ └─ArgumentsList (line 1, col 50) + ├─LoopParameters (line 1, col 14) + │ └─Identifier (line 1, col 14) + │ └'i' + └─NoneNode (line 0, col 0) +``` + +Technically SMNP does have **LL(1)** parser implemented. +The acronym means: +* input is read from **L**eft to right +* parser produces a **L**eft-to-right derivation +* parser uses one lookahead token. +Even though this kind of parsers is treated as the least sophisticated, in most cases +they do the job and are enough even for more advanced use cases. + +SMNP language parser has some fundamental helper function that provides +something like construction blocks that are used in right production +rules implementations. SMNP language parser actually is a combination +of sub-parsers that are able to parse subset of language. + +For example `smnp/ast/atom.py` file contains parsers related to parsing +atomic values, like literals and so on (note also that *expression* with parentheses +on both sides is treated like atom). +The file defines `AtomParser` which is actually a function that takes list +of tokens and produces AST. In this case `AtomParser` is able to parse only token +sequences mentioned before, like string literals, integer literals etc. + +But `AtomParser` is used by `UnitParser` located in `smnp/ast/unit.py`. +`UnitParser` introduces another production rules, like unary minus (`-`) operator +and dot (`.`) operator (which is used to accessing fields and methods). +In the first case, production is implemented using `Parser.allOf()` helper function +which implements *and*-operation (conjunction) performed between symbols. +```python +# smnp/ast/unit.py +minusOperator = Parser.allOf( + Parser.terminal(TokenType.MINUS, createNode=Operator.withValue), + Parser.doAssert(AtomParser, "atom"), + createNode=MinusOperator.withValues, + name="minus" +) + +# this implements following production rule: +# minusOperator ::= "-" atom +``` + +Next production rule of `UnitParser` produces `atom2` symbol using `Parser.oneOf()` helper function which +implements *or*-operation (alternative) performed between symbols: +```python +atom2 = Parser.oneOf( + minusOperator, + AtomParser, + name="atom2" +) + +# which implements following production rule: +# atom2 ::= minusOperator | atom +``` + +The last production rule of `UnitParser` is related to `unit` symbol and implements dot operator: +```python +Parser.leftAssociativeOperatorParser( + atom2, + [TokenType.DOT], + Parser.doAssert(atom2, "atom"), + createNode=lambda left, op, right: Access.withValues(left, op, right), + name="unit" +) + +# which implements following production rule: +# unit ::= atom2 | atom2 "." atom2 +# (leftAssociativeOperator() is function that implements production rule of left-associative operator) +``` + +`UnitParser` is then used by `FactorParser` (`smnp/ast/factor.py`) which defines power operator (`**`) +and logic negation operator (`not`). `FactorParser` is used by `TermParser` (`smnp/ast/term.py`) and `TermParser` is used in turn +by `ExpressionParser` (`smnp/ast/expression.py`). +It is also worth paying attention to the impact of cascade of production rules on operators precedence. + +As you can see, almost each parser is cascadingly composed from another parsers with consistently increased +supported subset of SMNP language. Thanks to the design, parsers are quite easy to debug and test. + +All parser-related code is located in `smnp/ast` module. + +### Evaluator +Evaluator is the last stage of SMNP language processing pipeline and also is the heart +of all SMNP tool, which takes AST as input and performs programmed operations. +Similar to implemented parser, evaluator works *recursively* because of processing tree-like +structure. +Evaluator's architecture is similar to parser's one. Evaluator consists of +smaller evaluators which are able to evaluate small part of AST's node types. + +Because evaluator introduces as *runtime* term, it also works on special object +called *environment*. The *environment* object contains some runtime information, +like available functions (both coming from *stdlib* and user-defined), scopes +(stack of defined variables), call stack and some meta information, like +source of code. This object is passed through all evaluators along with AST and its +subtrees. + +Example of `and` and `or` operators evaluators: +```python +# smnp/runtime/evaluators/logic.py +class AndEvaluator(Evaluator): + + @classmethod + def evaluator(cls, node, environment): + left = expressionEvaluator(doAssert=True)(node.left, environment).value + right = expressionEvaluator(doAssert=True)(node.right, environment).value + return Type.bool(left.value and right.value) + + +class OrEvaluator(Evaluator): + + @classmethod + def evaluator(cls, node, environment): + left = expressionEvaluator(doAssert=True)(node.left, environment).value + right = expressionEvaluator(doAssert=True)(node.right, environment).value + return Type.bool(left.value or right.value) +``` + +Above evaluators are bound to `And` and respectively `Or` AST nodes in `smnp/runtime/evaluators/expression.py` file, +which defines `ExpressionEvaluator`: +```python +result = Evaluator.oneOf( + # (...) + Evaluator.forNodes(AndEvaluator.evaluate, And), + Evaluator.forNodes(OrEvaluator.evaluate, Or), + AtomEvaluator.evaluate +)(node, environment) +``` +The `ExpressionEvaluator` uses `Evaluator.oneOf()` helper function which +tries to evaluate given node using one of defined evaluators. +`Evaluator.forNodes()` helper function in turn wraps evaluator to condition +which checks if node is one of given node types that are accepted by evaluator. + +`ExpressionEvaluator` among with some other evaluators is used in main evaluator +in `smnp/runtime/evaluator.py` file. + +Evaluator can be disabled using `--dry-run` flag. +All evaluator-related code is located in `smnp/runtime` module. + +### Interpreter +`Interpreter` actually isn't an another language processing +stage, rather it is a facade that composes each stage in one pipeline, +accepting a raw SMNP code as input. It also imports +each function and method from SMNP modules (`smnp/module` module, which is actually +a *SMNP native library*) and includes +it to passed `environment` object. As long as environment containing functions and methods +that come from *SMNP language library* is created in `smnp/main.py` file, +it is passed as argument to `_interpret()` function as `baseEnvironment` and extends +newly created `environment` object with its functions and methods. From now +`environment` object contains both *SMNP native library*'s and *SMNP language library*'s +functions and methods. + +Snippet below contains `_interpret()` function that composes pipeline of stages +mentioned before: +```python +# smnp/program/interpreter.py +# (...) +from smnp.module import functions, methods +# (...) + +class Interpreter: + # (...) + + @staticmethod + def _interpret(lines, source, printTokens=False, printAst=False, execute=True, baseEnvironment=None): + environment = Environment([{}], functions, methods, source=source) + + if baseEnvironment is not None: + environment.extend(baseEnvironment) + + try: + tokens = tokenize(lines) + if printTokens: + print(tokens) + + ast = parse(tokens) + if printAst: + ast.print() + + if execute: + evaluate(ast, environment) + + return environment + except RuntimeException as e: + e.environment = environment + e.file = environment.source + raise e +``` +That's the main flow of SMNP code execution. + +## Audio module +Audio module consists so far of 3 elements including 2 output modules (which +produces a song) and 1 input module (which work is based on microphone input). + +### `sound` type +`sound` type was totally covered in previous chapter, so there is nothing to say more. +You are able to load music and play it via `play()` method. +Technically this type uses *SoundFile* library which is responsible for loading +and playing music, so `sound` type actually is a wrapper for Python class provided +by *SoundFile* framework. + +### Synthesising module +In oppose to `sound` type, synthesising module is **designed and implemented from scratch**. +The only 3rd party code is provided with two frameworks: +* *SoundDevice* library, which actually +is an abstract layer above hardware and allows SMNP to pass custom data to sound card in order +to play music through speakers +* *Numpy* framework, which provides a special kind of list (*Numpy*'s array) that +is the only container type accepted by *SoundDevice*. + +#### Synthesising process +Synthesising process consists of two stages: compiling notes and playing wave. +Depending on your needs the stages can be performed separately in different functions +or merged to just one call. + +First of all you need to compile notes in order to have wave (which is of `list` type) using `wave` function. +Then already compiled wave can be finally synthesised with `synth` function. +``` +notes = [@c, @d, @e, @f]; +compiledWave = wave(notes); +synth(compiledWave); + +``` +Above code is really simple, so you can get rid of `wave` function invocation +and pass notes directly to `synth` function: +``` +synth([@c, @d, @e, @f]); +``` +And that's it! Both above codes will play following notes: +![cdef](img/notes/cdef.png) + +##### Rests +Rest is nothing but `integer` placed among the notes. +Value of number determines rest's length and works similar to duration part of note literal. +For example: +* `1` - whole rest +* `2` - half rest +* `4` - quarter rest +* `8` - eighth rest +* `16` - sixteenth rest +* `32` - thirty-second rest +* `64` - sixty-fourth rest +* etc. + +As long as rests are represented as integers, they don't supports dots which make them +longer with half of value, however you dot can be specified as another rest with +half value of previous one, for example: dotted half pause = `[2, 4]`. + +Example of using rests: +``` +notes = [@c, @d, 4, @f, @g, @a, 4, @c5]; +synth(notes); +``` +This code will play following notes: +![cd4fga8c](img/notes/cd4fga8c.png) + +##### Polyphony +You can have as many note staffs as you want and you are still able to merge them to one +wave, that can be played later. + +Both `wave` function and `synth` function accepts multiple lists of notes +as arguments. If you pass more than one list, all of them will be merged +and normalised to achieve polyphonic effect. + +Let's say you have following voices: +* ![twinkle1](img/notes/twinkle1.png) +* ![twinkle2](img/notes/twinkle2.png) + +Notes above can be merged to one wave, as follows: +``` +twinkle1 = [@c, @c, @g, @g, @a, @a, @g:2]; +twinkle2 = [@c, @c, @e, @e, @f, @f, @e:2]; +twinkle = wave(twinkle1, twinkle2); +synth(twinkle); + +# or just +synth(twinkle1, twinkle2); +``` +The result is: +![twinkle3](img/notes/twinkle3.png) + +**Remember**, that in order to achieve the effect you have to pass `list` arguments, +even if they will consist just of one element. + +Example: +``` +synth([@c], [@e], [@g], [@c5]); +synth(@c, @e, @g, @c5); +``` +The first line represents following notes: +![poly1](img/notes/poly1.png) + +but the second line is not polyphonic anymore: +![poly2](img/notes/poly2.png) + +##### Tuplets +Tuplets can be achieved with `tuplet` function, that comes from *stdlib*. +First argument of function determines how many notes +you can put, the second one determines how many notes will be +replaced and the last argument is vararg containing desired notes. + +Example: +``` +notes = [@g:2, @d5:2] + tuplet(3, 2, @c5:8, @h:8, @a:8) + [@g5:2, @d5] +synth(notes) +``` +the code will play following notes: +![starwars](img/notes/starwars.png) +Instead of 2 eighth notes (the second argument of `tuplet` function) +we are going to have 3 eighth notes (the first argument of `tuplet` function). + +Because `tuplet` function produces a list of notes, and both `synth` and +`wave` functions expect flat lists (i.e. lists that don't contain another lists) +there is need to concatenate 3 lists in order to have just one. +Another option is to include `tuplet` function to list in order to have +nested lists and then use `flat` function, as follows: +``` +notes = [@g:2, @d5:2, tuplet(3, 2, @c5:8, @h:8, @a:8), @g5:2, @d5] +synth(flat(notes)) +``` + +#### Synthesising configuration +Both `wave` and `synth` function have overloaded version that accepts +a config as first argument. + +The config value is actually of `map<>` type and +can include following keys: +* `bpm: integer` +* `tuning: integer` +* `overtones: list` +* `attack: float` +* `decay: float` + +Parameters above will be covered in this section. All keys included +to config map that don't match presented parameters +will be ignored. + +Remember, that the config argument is just additional argument +and are rules presented in previous section are applicable here as well. + +Example: +``` +w = wave({ bpm -> 270 }, @c, @d, @e, @f); + +w2 = wave({ bpm -> 60, tuning -> 432 }, + [@e, @f, @e, @d, @e:2, 2], + transpose(-12, [@g, @a, @g, @g, @g:2, 2]), + transpose(-12, [@c, @c, @c:4d, @h3:8, @c:2, 2]) +); +``` + +##### Tempo +Tempo can be adjusted with `bpm` property. +BPM means beats per minute and determines (in this case) how many quarter notes +will be played during single minute. +For example: +* 60 bpm - 60 quarter notes per minute - one quarter note per second +* 120 bpm - 120 quarter notes per minute - two quarter notes per second +* and so on. + +``` +notes = noteRange(@c, @c5, "diatonic"); # C Major scale +synth({ bpm -> 60 }, notes); # notes will be being played during 8s +synth({ bpm -> 120 }, notes); # notes will be being played during 4s +synth({ bpm -> 240 }, notes); # notes will be being played drugin 2s +``` + +##### Tuning +`tuning` is the property that determines frequency of `@a` sound. +Typically it is set to 440Hz or 432Hz, but it can be any positive value. +Using this property you are able to adjust tuning of all available notes. + +Example: +``` +a4 = @a:128; +plot(wave({ tuning -> 440, overtones -> [1.0] }, a4)); +plot(wave({ tuning -> 127, overtones -> [1.0] }, a4)); +``` +![a_440](img/plots/a_440.png) +![a_127](img/plots/a_127.png) +Plots above presents fundamental tone's sine wave of the same note: `@a` +in different tuning systems. + +You can hear the difference executing following code: +``` +notes = noteRange(@c, @c5, "diatonic"); # C Major scale +synth({ tuning -> 440 }, notes); +synth({ tuning -> 512 }, notes); +``` + +##### Overtones +Overtones allows you to specify the sound of single note being compiled. +In fact each note can be composed from sine waves with greater and greater +frequencies. +`overtones` parameter is a list of float values, that represents +a quotient of influence each overtone to output wave's amplitude. +First value at the list is related to fundamental tone. Second one +is related to first overtone (second harmonic), the third one +is related to second overtone (third harmonic) and so on. +You can read more about overtones at [Wikipedia](https://en.wikipedia.org/wiki/Overtone). + +Sum of list of overtones' values must be less or equal to 1. +To disable overtone simply put `0.0` at proper place of list. +For example sound with `overtones = [0.7, 0.0, 0.3]` consists +only of fundamental tone with amplitude equal to 0.7 of total amplitude and +third harmonic with amplitude equal to 0.3 of total amplitude. No other overtones +are enabled. + +Examples (with related plots): + +1st example (1 overtone - ideal pitchfork sound) +``` +# v 1st +w = wave({ overtones -> [1.0] }, @a:64) # a = 440 Hz +plot(w) +plot(fft(w)) +``` +![a_1overtone](img/plots/a_1overtone.png) +![a_1overtone_fft](img/plots/a_1overtone_fft.png) + + +2nd example (2 overtones) +``` +# v 1st v 3rd +w = wave({ overtones -> [0.7, 0.0, 0.3] }, @a:64) +plot(w) +plot(fft(w)) +``` +![a_2overtones](img/plots/a_2overtones.png) +![a_2overtones_fft](img/plots/a_2overtones_fft.png) + +3rd example (3 overtones) +``` +# v 1st v 3rd v 14th +w = wave({ overtones -> [0.5, 0.0, 0.3] + (10 ^ 0.0) + [0.2] }, @a:64) +plot(w) +plot(fft(w)) +``` +![a_3overtones](img/plots/a_3overtones.png) +![a_3overtones_fft](img/plots/a_3overtones_fft.png) + +##### Decay +`decay` parameter defines the tail of sound. Value of the parameter +determines how fast the sound is going to be "blanked". +The greater value of `decay` parameter, the sound will be blanked faster. +If `decay` parameter is set to 0, sound will not change its magnitude anymore. + +Example (with proper plots): +``` +a4 = @a:16; +plot(wave({ decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 0.5, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 1, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 5, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 10, overtones -> [1.0] }, a4)); +``` +![decay0](img/plots/decay0.png) +![decay05](img/plots/decay05.png) +![decay1](img/plots/decay1.png) +![decay5](img/plots/decay5.png) +![decay10](img/plots/decay10.png) + +##### Attack +`attack` parameter is the last one supported by config map +and in oppose to `decay` determines how fast the sound is going to reach +full magnitude. +The greater value of `attack`, the sound will reach full magnitude faster. +If `attack` parameter is set to 0, the effect will be disabled and full +magnitude will be reached immediately. + +Example (with proper plots): +``` +a4 = @a:16; +plot(wave({ attack -> 0, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 1, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 5, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 10, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 100, decay -> 0, overtones -> [1.0] }, a4)); +``` +![attack0](img/plots/attack0.png) +![attack1](img/plots/attack1.png) +![attack5](img/plots/attack5.png) +![attack10](img/plots/attack10.png) +![attack100](img/plots/attack100.png) + +##### Mixing parameters +You are able to mix parameters and achieve really interesting results +to emulate for example piano, guitar, string ensemble etc. + +For example, you can achieve this sound's shape: +![example_config](img/plots/example_config.png) +using following code: +``` +config = { + attack -> 2, + decay -> 7, + overtones -> [0.5, 0.0, 0.0, 0.0, 0.3, 0.0, 0.2] +}; + +plot(wave(config, @a:64)); + +synth(config, [@c:1, @d:1, @e:1, @f:1], [2, @c5:1, @h:2, @c:1, @a:1]); +``` +It sounds really magically, isn't it? +Feel free to mix parameters in order to achieve desired sound +and enjoy it! + +##### Default values +If you don't pass any of supported parameters or even use +the simplest signature of `wave` or `synth` function, following default +values will be assigned to each parameter: +* `bpm = 120` +* `tuning = 440` +* `overtones = [0.4, 0.3, 0.1, 0.1, 0.1]` +* `decay = 4` +* `attack = 100` + +which causes following sound's shape: +``` +plot(wave(@a:64)) +``` +![default_config](img/plots/default.png) + +### Listening module +Listening module is the only implemented module that processes input +coming from microphone. +Moreover the module contains only one function which is `wait`. +The function accepts 2 integer arguments: `soundLevel` and `silenceLevel` and is useful to ear training. +The goal of `wait` function is to hang program execution and wait +for incoming sound. Only when the sound sounds and then stops, +the program's execution will be resumed. + +Function can be in three states. Let's call them: +* `WAIT_FOR_SOUND` +* `WAIT_FOR_SILENCE` +* `RESUME`. + +Now it is possible to show how it works using finite states machine: +![wait_fsm](img/schemas/wait_fsm.png) + +In other words, `wait` function waits for sound, which means +it hangs program execution and waits for exceeding `soundLevel` parameter by input level, +which is the first argument of `wait` function. +After that it waits for sound's stop (i.e. input level must be less than `silenceLevel`, which is +the second argument of `wait` function). +Only then `wait` function ends its work and resumes program's execution. + +If you are not sure about what `soundLevel` or `silenceLevel` values you need to use, +you can run SMNP program with `-m` flag in order to test in real time what kind of noise produces what level. + +Instead of calling function with two arguments, you can invoke it without any argument. +In that case: +``` +wait() +``` +is equivalent of +``` +wait(300, 10) +``` + +## Command-line interface +SMNP is provided as command-line tool and so far doesn't support +any graphic interface. +The simplest way to use SMNP is to type `smnp `, where `` +is a file containing SMNP language code. +You can put more files than one, just remember to split them with +spaces. +You are also able to execute SMNP language inline, which can be useful +in some kinds of batch scripts. +It can be done using `-c` option, like on following example: +``` +$ smnp -c "println(\"Hello, world!\")" +Hello, world! +$ +``` +Remember to escape all quotes if you are typing string literals +in this mode. + +Apart from this options mentioned so far, SMNP provides you another ones, +which you can list using `smnp -h` command. +All of them are also covered below: +* `-c ` - allows you to execute code passed as argument instead +of reading it from file +* `-m` - allows you to test input level coming from microphone +* `-h` - displays simple manual of SMNP usage with all options and +flags described +* `-v` - displays version of SMNP + +Options planned to be implemented: +* `-C ` (not implemented yet) - allows you to override +default file that contains common configuration of SMNP tool +* `-P =` (not implemented yet) - allows you to pass +properties that you will be able to obtain from code + +Because SMNP acts also as educational tool on field of parsing formal +languages, there are also implemented some flags allowing you +to see what is going on at each language processing stage: +* `--tokens` - prints tokenizer's output for passed code +* `--ast` - *pretty-prints* abstract syntax tree as parser's output for passed code +* `--dry-run` - runs language-processing tools without involving evaluator + diff --git a/img/notes/cd4fga8c.png b/img/notes/cd4fga8c.png new file mode 100644 index 0000000..fc76f96 Binary files /dev/null and b/img/notes/cd4fga8c.png differ diff --git a/img/notes/cdef.png b/img/notes/cdef.png new file mode 100644 index 0000000..59c5d0c Binary files /dev/null and b/img/notes/cdef.png differ diff --git a/img/notes/poly1.png b/img/notes/poly1.png new file mode 100644 index 0000000..417e945 Binary files /dev/null and b/img/notes/poly1.png differ diff --git a/img/notes/poly2.png b/img/notes/poly2.png new file mode 100644 index 0000000..045fef2 Binary files /dev/null and b/img/notes/poly2.png differ diff --git a/img/notes/starwars.png b/img/notes/starwars.png new file mode 100644 index 0000000..ecb7e88 Binary files /dev/null and b/img/notes/starwars.png differ diff --git a/img/notes/twinkle1.png b/img/notes/twinkle1.png new file mode 100644 index 0000000..ed4e7d7 Binary files /dev/null and b/img/notes/twinkle1.png differ diff --git a/img/notes/twinkle2.png b/img/notes/twinkle2.png new file mode 100644 index 0000000..19da26a Binary files /dev/null and b/img/notes/twinkle2.png differ diff --git a/img/notes/twinkle3.png b/img/notes/twinkle3.png new file mode 100644 index 0000000..fd4a361 Binary files /dev/null and b/img/notes/twinkle3.png differ diff --git a/img/plots/a_127.png b/img/plots/a_127.png new file mode 100644 index 0000000..79013c1 Binary files /dev/null and b/img/plots/a_127.png differ diff --git a/img/plots/a_1overtone.png b/img/plots/a_1overtone.png new file mode 100644 index 0000000..106aa4e Binary files /dev/null and b/img/plots/a_1overtone.png differ diff --git a/img/plots/a_1overtone_fft.png b/img/plots/a_1overtone_fft.png new file mode 100644 index 0000000..2cf2e6e Binary files /dev/null and b/img/plots/a_1overtone_fft.png differ diff --git a/img/plots/a_2overtones.png b/img/plots/a_2overtones.png new file mode 100644 index 0000000..8c2f4bb Binary files /dev/null and b/img/plots/a_2overtones.png differ diff --git a/img/plots/a_2overtones_fft.png b/img/plots/a_2overtones_fft.png new file mode 100644 index 0000000..fd320ca Binary files /dev/null and b/img/plots/a_2overtones_fft.png differ diff --git a/img/plots/a_3overtones.png b/img/plots/a_3overtones.png new file mode 100644 index 0000000..bd098e6 Binary files /dev/null and b/img/plots/a_3overtones.png differ diff --git a/img/plots/a_3overtones_fft.png b/img/plots/a_3overtones_fft.png new file mode 100644 index 0000000..f48a7fc Binary files /dev/null and b/img/plots/a_3overtones_fft.png differ diff --git a/img/plots/a_440.png b/img/plots/a_440.png new file mode 100644 index 0000000..b21eb34 Binary files /dev/null and b/img/plots/a_440.png differ diff --git a/img/plots/attack0.png b/img/plots/attack0.png new file mode 100644 index 0000000..5a62732 Binary files /dev/null and b/img/plots/attack0.png differ diff --git a/img/plots/attack1.png b/img/plots/attack1.png new file mode 100644 index 0000000..c5534a4 Binary files /dev/null and b/img/plots/attack1.png differ diff --git a/img/plots/attack10.png b/img/plots/attack10.png new file mode 100644 index 0000000..00daf12 Binary files /dev/null and b/img/plots/attack10.png differ diff --git a/img/plots/attack100.png b/img/plots/attack100.png new file mode 100644 index 0000000..a845d21 Binary files /dev/null and b/img/plots/attack100.png differ diff --git a/img/plots/attack5.png b/img/plots/attack5.png new file mode 100644 index 0000000..fb88b5b Binary files /dev/null and b/img/plots/attack5.png differ diff --git a/img/plots/decay0.png b/img/plots/decay0.png new file mode 100644 index 0000000..a845d21 Binary files /dev/null and b/img/plots/decay0.png differ diff --git a/img/plots/decay05.png b/img/plots/decay05.png new file mode 100644 index 0000000..e1eecb6 Binary files /dev/null and b/img/plots/decay05.png differ diff --git a/img/plots/decay1.png b/img/plots/decay1.png new file mode 100644 index 0000000..af454b5 Binary files /dev/null and b/img/plots/decay1.png differ diff --git a/img/plots/decay10.png b/img/plots/decay10.png new file mode 100644 index 0000000..5b2dd98 Binary files /dev/null and b/img/plots/decay10.png differ diff --git a/img/plots/decay5.png b/img/plots/decay5.png new file mode 100644 index 0000000..ddf18ab Binary files /dev/null and b/img/plots/decay5.png differ diff --git a/img/plots/default.png b/img/plots/default.png new file mode 100644 index 0000000..4397a57 Binary files /dev/null and b/img/plots/default.png differ diff --git a/img/plots/example_config.png b/img/plots/example_config.png new file mode 100644 index 0000000..939b0c1 Binary files /dev/null and b/img/plots/example_config.png differ diff --git a/img/schemas/wait_fsm.png b/img/schemas/wait_fsm.png new file mode 100644 index 0000000..c569f07 Binary files /dev/null and b/img/schemas/wait_fsm.png differ diff --git a/smnp/runtime/evaluators/function.py b/smnp/runtime/evaluators/function.py index 6b59ca1..a1a7875 100644 --- a/smnp/runtime/evaluators/function.py +++ b/smnp/runtime/evaluators/function.py @@ -51,7 +51,7 @@ class ReturnEvaluator(Evaluator): # Disclaimer # Exception system usage to control program execution flow is really bad idea. # However because of lack of 'goto' instruction equivalent in Python - # there is to need to use some mechanism to break function execution on 'return' statement + # there is a need to use some mechanism to break function execution on 'return' statement # and immediately go to Environment's method 'invokeFunction()' or 'invokeMethod()', # which can handle value that came with exception and return it to code being executed. else: diff --git a/smnp/runtime/evaluators/power.py b/smnp/runtime/evaluators/power.py index 5d9abb9..2239f15 100644 --- a/smnp/runtime/evaluators/power.py +++ b/smnp/runtime/evaluators/power.py @@ -18,4 +18,4 @@ class PowerEvaluator(Evaluator): if not right.type in supportedTypes: raise RuntimeException(f"Operator '{node.operator.value}' is supported only by {[t.name.lower() for t in supportedTypes]} type", node.right.pos) - return Type.integer(int(left.value ** right.value)) \ No newline at end of file + return Type.float(float(left.value ** right.value)) \ No newline at end of file