Introduced concurrent block notation

Additionally removed wierd behaviour with Interpreter::play where empty
chords were played as default length. Don't know why this was introduced
This commit is contained in:
Robert Bendun 2022-11-24 02:08:34 +01:00
parent c509b6ccc5
commit 532727b7d1
10 changed files with 146 additions and 81 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `{}` notation for concurrently executing blocks
### Fixed
- release build script was producing executable with wrong path

View File

@ -7,8 +7,8 @@ hand1_pool := (
hand2_pool := (d8, d#8, g8, g#8, d9, d#9),
concurrent
(while true (play ((pick hand1_pool) (pick hn dhn))))
(while true (play ((pick hand2_pool) (1/64))))
play {
while true ((pick hand1_pool) (pick hn dhn)),
while true ((pick hand2_pool) (1/64))
}

View File

@ -242,27 +242,27 @@ static auto builtin_program_change(Interpreter &i, std::vector<Value> args) -> R
/// Plays sequentialy notes walking into arrays and evaluation blocks
///
/// @invariant default_action is play one
static inline std::optional<Error> sequential_play(Interpreter &i, Value v)
static inline std::optional<Error> sequential_play(Interpreter &interpreter, Value v)
{
if (auto array = get_if<Array>(v)) {
for (auto &el : array->elements) {
Try(sequential_play(i, std::move(el)));
Try(sequential_play(interpreter, std::move(el)));
}
}
else if (auto block = get_if<Block>(v)) {
Try(sequential_play(i, Try(i.eval(std::move(block->body)))));
Try(sequential_play(interpreter, Try(interpreter.eval(std::move(block->body)))));
}
else if (auto chord = get_if<Chord>(v)) {
return i.play(*chord);
return interpreter.play(*chord);
}
return {};
}
/// Play what's given
static std::optional<Error> action_play(Interpreter &i, Value v)
static std::optional<Error> action_play(Interpreter &interpreter, Value v)
{
Try(sequential_play(i, std::move(v)));
Try(sequential_play(interpreter, std::move(v)));
return {};
}
@ -602,38 +602,6 @@ static Result<Value> builtin_scan(Interpreter &interpreter, std::vector<Value> a
};
}
static Result<Value> builtin_concurrent(Interpreter &interpreter, std::span<Ast> args)
{
auto const jobs_count = args.size();
std::vector<std::future<Value>> futures;
std::optional<Error> error;
std::mutex mutex;
for (unsigned i = 0; i < jobs_count; ++i) {
futures.push_back(std::async(std::launch::async, [interpreter = interpreter.clone(), i, args, &mutex, &error]() mutable -> Value {
auto result = interpreter.eval((Ast)args[i]);
if (result) {
return *std::move(result);
}
std::lock_guard guard{mutex};
if (!error) {
error = result.error();
}
return Value{};
}));
}
std::vector<Value> results;
for (auto& future : futures) {
if (error) {
return *error;
}
results.push_back(future.get());
}
return results;
}
/// Execute blocks depending on condition
static Result<Value> builtin_if(Interpreter &i, std::span<Ast> args) {
static constexpr auto guard = Guard<2> {
@ -666,7 +634,7 @@ static Result<Value> builtin_if(Interpreter &i, std::span<Ast> args) {
}
/// Loop block depending on condition
static Result<Value> builtin_while(Interpreter &i, std::span<Ast> args) {
static Result<Value> builtin_while(Interpreter &interpreter, std::span<Ast> args) {
static constexpr auto guard = Guard<2> {
.name = "while",
.possibilities = {
@ -678,11 +646,16 @@ static Result<Value> builtin_while(Interpreter &i, std::span<Ast> args) {
return guard.yield_error();
}
while (Try(i.eval((Ast)args.front())).truthy()) {
while (Try(interpreter.eval((Ast)args.front())).truthy()) {
Value result;
if (args[1].type == Ast::Type::Block) {
Try(i.eval((Ast)args[1].arguments.front()));
result = Try(interpreter.eval((Ast)args[1].arguments.front()));
} else {
Try(i.eval((Ast)args[1]));
result = Try(interpreter.eval((Ast)args[1]));
}
if (interpreter.default_action) {
Try(interpreter.default_action(interpreter, std::move(result)));
}
}
return Value{};
@ -1151,7 +1124,6 @@ void Interpreter::register_builtin_functions()
global.force_define("call", builtin_call);
global.force_define("ceil", apply_numeric_transform<&Number::ceil>);
global.force_define("chord", builtin_chord);
global.force_define("concurrent", builtin_concurrent);
global.force_define("down", builtin_range<Range_Direction::Down>);
global.force_define("duration", builtin_duration);
global.force_define("flat", builtin_flat);

View File

@ -6,6 +6,8 @@
#include <iostream>
#include <random>
#include <thread>
#include <future>
#include <mutex>
midi::Connection *Interpreter::midi_connection = nullptr;
std::unordered_map<std::string, Intrinsic> Interpreter::operators {};
@ -61,6 +63,50 @@ Interpreter::Interpreter(Interpreter::Clone)
{
}
static Result<Value> eval_concurrent(Interpreter &interpreter, Ast &&ast)
{
ensure(ast.type == Ast::Type::Concurrent, "Only conccurent AST nodes can be evaluated in eval_concurrent");
std::span<Ast> jobs = ast.arguments;
std::vector<std::future<Value>> futures;
std::optional<Error> error;
std::mutex mutex;
for (unsigned i = 0; i < jobs.size(); ++i) {
futures.push_back(std::async(std::launch::async, [interpreter = interpreter.clone(), i, jobs, &mutex, &error]() mutable -> Value {
auto result = interpreter.eval(std::move(jobs[i]));
if (result) {
if (interpreter.default_action) {
// TODO Code duplication between this section and last section in this lambda function
if (auto produced_error = interpreter.default_action(interpreter, *std::move(result))) {
std::lock_guard guard{mutex};
if (!error) {
error = produced_error;
}
return Value{};
}
}
return *std::move(result);
}
std::lock_guard guard{mutex};
if (!error) {
error = result.error();
}
return Value{};
}));
}
std::vector<Value> results;
for (auto& future : futures) {
if (error) {
return *error;
}
results.push_back(future.get());
}
return results;
}
Result<Value> Interpreter::eval(Ast &&ast)
{
switch (ast.type) {
@ -163,6 +209,9 @@ Result<Value> Interpreter::eval(Ast &&ast)
}
break;
case Ast::Type::Concurrent:
return eval_concurrent(*this, std::move(ast));
case Ast::Type::Sequence:
{
Value v;
@ -243,7 +292,6 @@ std::optional<Error> Interpreter::play(Chord chord)
auto &ctx = *current_context;
if (chord.notes.size() == 0) {
std::this_thread::sleep_for(ctx.length_to_duration(ctx.length));
return {};
}
@ -300,7 +348,9 @@ static void snapshot(std::ostream& out, Note const& note) {
static void snapshot(std::ostream &out, Ast const& ast) {
switch (ast.type) {
break; case Ast::Type::Sequence:
break;
case Ast::Type::Sequence:
case Ast::Type::Concurrent:
{
for (auto const& a : ast.arguments) {
snapshot(out, a);
@ -308,7 +358,10 @@ static void snapshot(std::ostream &out, Ast const& ast) {
}
}
break; case Ast::Type::Block:
// TODO
ensure(ast.arguments.size() == 1, "Block can contain only one node which contains its body");
if (ast.arguments.front().type == Ast::Type::Concurrent)
unimplemented("Concurrent code snapshoting is not supported yet");
out << "(";
snapshot(out, ast.arguments.front());
out << ")";

View File

@ -88,8 +88,10 @@ auto Lexer::next_token() -> Result<std::variant<Token, End_Of_File>>
}
switch (peek()) {
case '(': consume(); return Token { Token::Type::Open_Block, finish(), token_location };
case ')': consume(); return Token { Token::Type::Close_Block, finish(), token_location };
case '(': consume(); return Token { Token::Type::Open_Sequential, finish(), token_location };
case ')': consume(); return Token { Token::Type::Close_Sequential, finish(), token_location };
case '{': consume(); return Token { Token::Type::Open_Concurrent, finish(), token_location };
case '}': consume(); return Token { Token::Type::Close_Concurrent, finish(), token_location };
case '[': consume(); return Token { Token::Type::Open_Index, finish(), token_location };
case ']': consume(); return Token { Token::Type::Close_Index, finish(), token_location };
case ',': consume(); return Token { Token::Type::Expression_Separator, finish(), token_location };
@ -257,16 +259,18 @@ std::ostream& operator<<(std::ostream& os, Token::Type type)
{
switch (type) {
case Token::Type::Chord: return os << "CHORD";
case Token::Type::Close_Block: return os << "CLOSE BLOCK";
case Token::Type::Close_Concurrent: return os << "CLOSE CONCURRENT";
case Token::Type::Close_Index: return os << "CLOSE INDEX";
case Token::Type::Close_Sequential: return os << "CLOSE SEQUENTIAL";
case Token::Type::Expression_Separator: return os << "EXPRESSION SEPARATOR";
case Token::Type::Keyword: return os << "KEYWORD";
case Token::Type::Numeric: return os << "NUMERIC";
case Token::Type::Open_Block: return os << "OPEN BLOCK";
case Token::Type::Open_Concurrent: return os << "OPEN CONCURRENT";
case Token::Type::Open_Index: return os << "OPEN INDEX";
case Token::Type::Open_Sequential: return os << "OPEN SEQUENTIAL";
case Token::Type::Operator: return os << "OPERATOR";
case Token::Type::Parameter_Separator: return os << "PARAMETER SEPARATOR";
case Token::Type::Symbol: return os << "SYMBOL";
case Token::Type::Open_Index: return os << "OPEN INDEX";
case Token::Type::Close_Index: return os << "CLOSE INDEX";
}
unreachable();
}
@ -275,13 +279,15 @@ std::string_view type_name(Token::Type type)
{
switch (type) {
case Token::Type::Chord: return "chord";
case Token::Type::Close_Block: return ")";
case Token::Type::Close_Concurrent: return "}";
case Token::Type::Close_Index: return "]";
case Token::Type::Close_Sequential: return ")";
case Token::Type::Expression_Separator: return "|";
case Token::Type::Keyword: return "keyword";
case Token::Type::Numeric: return "numeric";
case Token::Type::Open_Block: return "(";
case Token::Type::Open_Concurrent: return "{";
case Token::Type::Open_Index: return "[";
case Token::Type::Open_Sequential: return "(";
case Token::Type::Operator: return "operator";
case Token::Type::Parameter_Separator: return "parameter separator";
case Token::Type::Symbol: return "symbol";

View File

@ -17,10 +17,12 @@ struct Token
Numeric, ///< numeric literal (floating point or integer)
Parameter_Separator, ///< "|" separaters arguments from block body
Expression_Separator, ///< "," separates expressions. Used mainly to separate calls, like `foo 1 2; bar 3 4`
Open_Block, ///< "(" starts anonymous block of code (potentially a function)
Close_Block, ///< ")" ends anonymous block of code (potentially a function)
Open_Index, ///< "[" starts index section of index expression
Close_Index ///< "]" ends index section of index expression
Close_Index, ///< "]" ends index section of index expression
Open_Sequential, ///< "(" starts anonymous sequential block of code (potentially a function)
Close_Sequential, ///< ")" ends anonymous sequential block of code (potentially a function)
Open_Concurrent, ///< "{" starts anonymous concurrent block
Close_Concurrent, ///< "}" ends anonymous concurrent block
};
/// Type of token

View File

@ -12,19 +12,22 @@ struct Ast
static Ast binary(Token, Ast lhs, Ast rhs);
/// Constructs block
static Ast block(Location location, Ast seq = sequence({}));
static Ast block(Location location, Ast seq);
/// Constructs call expression
static Ast call(std::vector<Ast> call);
/// Constructs block with parameters
static Ast lambda(Location location, Ast seq = sequence({}), std::vector<Ast> parameters = {});
static Ast lambda(Location location, Ast body = sequential({}), std::vector<Ast> parameters = {});
/// Constructs constants, literals and variable identifiers
static Ast literal(Token);
/// Constructs sequence of operations
static Ast sequence(std::vector<Ast> call);
static Ast sequential(std::vector<Ast> call);
/// Constructs concurrent collection of operations
static Ast concurrent(std::vector<Ast> ops);
/// Constructs variable declaration
static Ast variable_declaration(Location loc, std::vector<Ast> lvalues, std::optional<Ast> rvalue);
@ -38,6 +41,7 @@ struct Ast
Call, ///< Function call application like `print 42`
Literal, ///< Compile time known constant like `c` or `1`
Sequence, ///< Several expressions sequences like `42`, `42; 32`
Concurrent, ///< Conccurrent collection of expressions like inside {} block
Variable_Declaration, ///< Declaration of a variable with optional value assigment like `var x = 10` or `var y`
};

View File

@ -65,10 +65,11 @@ Result<Ast> Parser::parse(std::string_view source, std::string_view filename, un
}
}
auto const result = parser.parse_sequence();
auto const result = parser.parse_sequence(true);
if (result.has_value() && parser.token_id < parser.tokens.size()) {
if (parser.expect(Token::Type::Close_Block)) {
// FIXME There should be also check for closing index ] and closing concurrent block }
if (parser.expect(Token::Type::Close_Sequential)) {
auto const tok = parser.consume();
return Error {
.details = errors::Closing_Token_Without_Opening {
@ -84,10 +85,10 @@ Result<Ast> Parser::parse(std::string_view source, std::string_view filename, un
return result;
}
Result<Ast> Parser::parse_sequence()
Result<Ast> Parser::parse_sequence(bool is_sequential)
{
auto seq = Try(parse_many(*this, &Parser::parse_expression, Token::Type::Expression_Separator, At_Least::Zero));
return Ast::sequence(std::move(seq));
return is_sequential ? Ast::sequential(std::move(seq)) : Ast::concurrent(std::move(seq));
}
Result<Ast> Parser::parse_expression()
@ -185,10 +186,11 @@ Result<Ast> parse_sequence_inside(
Location start_location,
bool is_lambda,
std::vector<Ast> &&parameters,
auto &&dont_arrived_at_closing_token
auto &&dont_arrived_at_closing_token,
bool is_sequential
)
{
auto ast = Try(parser.parse_sequence());
auto ast = Try(parser.parse_sequence(is_sequential));
if (not parser.expect(closing_token)) {
Try(dont_arrived_at_closing_token());
return Error {
@ -206,7 +208,7 @@ Result<Ast> parse_sequence_inside(
return Ast::lambda(start_location, std::move(ast), std::move(parameters));
}
ensure(ast.type == Ast::Type::Sequence, "I dunno if this is a valid assumption tbh");
ensure(ast.type == Ast::Type::Sequence || ast.type == Ast::Type::Concurrent, "I dunno if this is a valid assumption tbh");
if (ast.arguments.size() == 1) {
return std::move(ast.arguments.front());
}
@ -231,7 +233,8 @@ Result<Ast> Parser::parse_index_expression()
{},
[]() -> std::optional<Error> {
return std::nullopt;
}
},
true
))
);
}
@ -258,12 +261,16 @@ Result<Ast> Parser::parse_atomic_expression()
case Token::Type::Symbol:
return Ast::literal(consume());
case Token::Type::Open_Block:
case Token::Type::Open_Sequential:
case Token::Type::Open_Concurrent:
{
auto opening = consume();
if (expect(Token::Type::Close_Block)) {
auto const opening = consume();
bool const is_sequential = opening.type == Token::Type::Open_Sequential;
auto const closing_token_type = is_sequential ? Token::Type::Close_Sequential : Token::Type::Close_Concurrent;
if (expect(closing_token_type)) {
consume();
return Ast::block(std::move(opening).location);
return Ast::block(std::move(opening).location, is_sequential ? Ast::sequential({}) : Ast::concurrent({}));
}
auto start = token_id;
@ -286,7 +293,7 @@ Result<Ast> Parser::parse_atomic_expression()
return parse_sequence_inside(
*this,
Token::Type::Close_Block,
closing_token_type,
opening.location,
is_lambda,
std::move(parameters),
@ -321,7 +328,8 @@ Result<Ast> Parser::parse_atomic_expression()
};
}
return std::nullopt;
}
},
is_sequential
);
}
@ -490,7 +498,7 @@ Ast Ast::call(std::vector<Ast> call)
return ast;
}
Ast Ast::sequence(std::vector<Ast> expressions)
Ast Ast::sequential(std::vector<Ast> expressions)
{
Ast ast;
ast.type = Type::Sequence;
@ -501,6 +509,17 @@ Ast Ast::sequence(std::vector<Ast> expressions)
return ast;
}
Ast Ast::concurrent(std::vector<Ast> expressions)
{
Ast ast;
ast.type = Type::Concurrent;
if (!expressions.empty()) {
ast.location = expressions.front().location;
ast.arguments = std::move(expressions);
}
return ast;
}
Ast Ast::block(Location location, Ast seq)
{
Ast ast;
@ -586,6 +605,10 @@ bool operator==(Ast const& lhs, Ast const& rhs)
case Ast::Type::Lambda:
case Ast::Type::Sequence:
case Ast::Type::Variable_Declaration:
// TODO Maybe concurrent blocks should resolve duplicates at AST level, and not execution level?
// Statements { play c, play c } should be parse time known to be { play c } I think.
// Anyway thing to reconsider later
case Ast::Type::Concurrent:
return lhs.arguments.size() == rhs.arguments.size()
&& std::equal(lhs.arguments.begin(), lhs.arguments.end(), rhs.arguments.begin());
}
@ -603,6 +626,7 @@ std::ostream& operator<<(std::ostream& os, Ast::Type type)
case Ast::Type::Literal: return os << "LITERAL";
case Ast::Type::Sequence: return os << "SEQUENCE";
case Ast::Type::Variable_Declaration: return os << "VAR";
case Ast::Type::Concurrent: return os << "CONCURRENT";
}
unreachable();
}

View File

@ -20,7 +20,7 @@ struct Parser
static Result<Ast> parse(std::string_view source, std::string_view filename, unsigned line_number = 0);
/// Parse sequence, collection of expressions
Result<Ast> parse_sequence();
Result<Ast> parse_sequence(bool is_sequential);
/// Parse either infix expression or variable declaration
Result<Ast> parse_expression();

View File

@ -14,8 +14,6 @@ struct Value;
/// Lazy Array / Continuation / Closure type thingy
struct Block : Collection, Function
{
~Block() override = default;
/// Location of definition / creation
Location location;
@ -28,6 +26,8 @@ struct Block : Collection, Function
/// Context from which block was created. Used for closures
std::shared_ptr<Env> context;
~Block() override = default;
/// Calling block
Result<Value> operator()(Interpreter &i, std::vector<Value> params) const override;