All Riddle values are first C++ values; the operations that can be performed on them are inherited from what the C++ value can, in a mostly straightforward way.
The Null type is represented by the single stateless object null.
The Bool type, with underlying type bool, is represented by the values
true and false.
The Int type, with underlying type long, is represented by base ten numbers
with no decimal part or exponent,
The Float type, with underlying type double, is represented by base ten
numbers with a decimal part, exponent or both.
The String type, with underlying type std::string, is represented by
double-quote-enclosed character sequences.
The List type, with underlying type std::vector<riddle::Val>, is
represented by bracket-enclosed, comma-separated value lists such as [],
[1], [1, 2, 3].
The Map type, with underlying type std::map<riddle::Val,riddle::Val>, is
represented by curly-brace-enclosed, comma-separated list of key:value
pairs such as {}, {1:2}, {"pi":3.14159, "e":2.71828}.
The Function type is in fact not one type, but a collection of types that
can be invoked on the C++ side with the arguments (riddle::Val*, riddle::Val*,
unsigned) to return an unsigned value. This covers both functions declared
in a script and built-in functions exported from the C++ side of things.
Any other C++ value can appear in a Riddle script by being exported to a scope, passed as an argument, or returned by a function.
All values can be tested as boolean values in the context of a conditional statement:
null value always evaluates to false.false if empty,
true otherwise.true.All objects can be compared with the ==, !=, <, >, <=, and >=
operators.
Note that comparing the addresses means that there will always be a valid comparison operator implementation and it will return the same values in the same run, but the comparison itself might return different values in different runs when objects were allocated in different places.
The basic binary arithmetic operators (+, -, *, /) are supported in a
case-by-case basis:
Additional binary operators (%, <<, >>, &, |, ^) are implemented
similarly, except the last step only attempts to cast the right side operand to
an integer.
Unary operators (~, -) are implemented if the type supports them.
Sequence types (those satisfying std::ranges::forward_range) implement
additional functions:
The size() operator that returns the size of the range. Note that this is
implemented on the C++ side but no such operator is provided by default in the
global scope.
The subscript operator sequence[n] is generated as follows:
sequence[x] is valid for an x of
type riddle::Val, the operator uses that instead.nth element from the beginning is accessed, otherwise the 1 -
nth element from the end is accessed.Sequence iteration is also handled automatically; from the point of view of the
script, it means any sequence object can be used as the iterable value in a
for statement; internally, the next operator is a function that takes a
sequence and an iterator, and returns the iterator to the next element, or
null if the element is the last one. If the iterator passed to the function
is null, it returns the start of the sequence.
All types are printable; usefulness of the printed data might vary though:
Null value is printed as null.Bool values are printed as either true or false.std::integral or std::floating_point is printed as a
number.std::ranges::forward_range…") string if it holds char; this
includes String by default.{, }), comma-separated (,),
key:value list if it holds pairs of elements; this includes Map by
default.[, ]), comma-separated (,) list
otherwise; this includes List by default.<Type>.Printing ranges could result in a nasty bit of recursion if an object holds a reference to itself somewhere inside it, so ranges are only fully printed if they are not being printed as part of their parents being printed themselves, otherwise a reduced form that does not show the elements is used.
By default, a type name is obtained by calling typeid(T).name(); in some
environments this will result in a more or less mangled name; and in many cases
the name will be too elaborate to be useful. The system allows users to provide
specializations to the riddle::Type_name struct that give shorter type names
for specific types.
The system also allows users to provide specializations to the riddle::Print
template to provide custom printing functions to any type.
A Riddle function can be defined with this syntax:
fun name(arg1, arg2, ..., argN):
code block
The function is declared in the scope where it is created, which can be the
body of yet another function. The function is an object originally bound to the
variable name in the scope where it has been declared, and callable with the
expression name(args...).
Variables in the parent scope are automatically captured by the child scope, so you can build something like this:
fun foo(arg):
fun impl(): return arg
return impl
And it will return a function that returns whichever value of arg you passed
to the original.
By default, Riddle will generate the call operator for any value that can be
invoked with arguments of type const Val*, Val*, int and returns Val. The
first argument is a pointer to the environment variables in the global scope,
the second a pointer to an array of arguments, and the third the number of
arguments with which the function is called.
Riddle also provides some facilities to generate an type with that
implementation of operator() for any callable object.
Use riddle::make_fun() to generate a callable object that deduces the types
and number of arguments from one function:
struct Foo { int bar; }
Foo foo(int a) { return Foo(a); }
auto val = riddle::Val(riddle::make_fun(&foo));
>>> foo(1)
<Foo>
Use riddle::make_funs() to generate a callable object that deduces the types
and number of arguments from several functions and calls the first one whose
arguments match:
struct Foo { int bar, baz; }
int foo_1(int a) { return Foo(a, 0); }
int foo_2(int a, int b) { return Foo(a, b); }
auto val = riddle::Val(riddle::make_funs(&foo_1, &foo_2));
>>> foo(1)
<Foo>
>>> foo(1,2)
<Foo>
Use riddle::make_field() to generate a callable object that works as a getter
or setter for a field of a C++ object, depending on the number of arguments you
call it with:
auto val = riddle::Val(riddle::make_field(&Foo::bar));
>>> f = Foo(1)
<Foo>
>>> bar(f, 2)
2
>>> bar(f)
2