If you look at the R3-Alpha implementation of REDUCE, it has two parts.
One part is a spec in a natives list (in %natives.r):
reduce: native [
{Evaluates expressions and returns multiple results.}
value
/no-set {Keep set-words as-is. Do not set them.}
/only {Only evaluate words and paths, not functions}
words [block! none!] {Optional words that are not evaluated (keywords)}
/into {Output results into a block with no intermediate storage}
out [any-block!]
]
The second part is the C code (in %n-control.c):
/***********************************************************************
**
*/ REBNATIVE(reduce)
/*
***********************************************************************/
{
if (IS_BLOCK(D_ARG(1))) {
REBSER *ser = VAL_SERIES(D_ARG(1));
REBCNT index = VAL_INDEX(D_ARG(1));
REBVAL *val = D_REF(5) ? D_ARG(6) : 0;
if (D_REF(2))
Reduce_Block_No_Set(ser, index, val);
else if (D_REF(3))
Reduce_Only(ser, index, D_ARG(4), val);
else
Reduce_Block(ser, index, val);
return R_TOS;
}
return R_ARG1;
}
Let's Reimagine This The Ren-C Way...
I'll start out just by doing the mechanical and aesthetic improvements of the build process, naming, and conventions.
Ready?
D_ARG(n) => ARG(NAME) (At Zero Runtime Cost)
![]()
/***********************************************************************
**
*/ REBNATIVE(reduce)
/*
***********************************************************************/
{
INCLUDE_PARAMS_OF_REDUCE;
if (IS_BLOCK(ARG(VALUE))) {
REBSER *ser = VAL_SERIES(ARG(VALUE)); // e.g. not VAL_SERIES(ARG(1))
REBCNT index = VAL_INDEX(ARG(VALUE));
REBVAL *val = REF(INTO) ? ARG(OUT) : 0;
if (REF(NO_SET))
Reduce_Block_No_Set(ser, index, val);
else if (REF(ONLY))
Reduce_Only(ser, index, ARG(WORDS), val);
else
Reduce_Block(ser, index, val);
return R_TOS;
}
return R_ARG1;
}
The first clarifying change is for each native, have the make prep step automatically generate a macro (e.g. INCLUDE_PARAMS_OF_REDUCE) that when you put it in the body of the function does several DECLARE_PARAM() lines that declare constants that can be found by the ARG() and REF() macros.
These INCLUDE_PARAMS_OF definitions look like this:
#define INCLUDE_PARAMS_OF_REDUCE \
DECLARE_PARAM(VALUE, 1); \
DECLARE_PARAM(NO_SET, 2); \
DECLARE_PARAM(ONLY, 3); \
DECLARE_PARAM(WORDS, 4); \
DECLARE_PARAM(INTO, 5); \
DECLARE_PARAM(OUT, 6)
DECLARE_PARAM makes a local enum definition:
#define DECLARE_PARAM(name,n) \
enum { param_##name##_ = (n) } // enums force compile-time const
Then we change:
#define D_ARG(n) (ds+(DSF_SIZE+n))
To be:
#define ARG(name) (ds + (DSF_SIZE + param_##name##_))
Note that these constants have no runtime cost--they are exactly as cheap as the hardcoded integers!
They can't be macros themselves (since they're scoped per-function). And const int might not be optimized out by a bad compiler--or even a good one at some optimization levels. So they're implemented as local enum declarations, which are guaranteed (for all practical purposes in any compiler you'd ever actually encounter) to act just like you typed the number directly into source.
Now Let's Put The Spec In The C File
![]()
//
// reduce: native [
// {Evaluates expressions and returns multiple results.}
// value
// /no-set {Keep set-words as-is. Do not set them.}
// /only {Only evaluate words and paths, not functions}
// words [block! none!] {Optional words that are not evaluated (keywords)}
// /into {Output results into a block with no intermediate storage}
// out [any-block!]
// ]
//
DECLARE_NATIVE(REDUCE)
{
INCLUDE_PARAMS_OF_REDUCE;
if (IS_BLOCK(ARG(VALUE))) {
REBSER *ser = VAL_SERIES(ARG(VALUE));
REBCNT index = VAL_INDEX(ARG(VALUE));
REBVAL *val = REF(INTO) ? ARG(OUT) : 0;
if (REF(NO_SET))
Reduce_Block_No_Set(ser, index, val);
else if (REF(ONLY))
Reduce_Only(ser, index, ARG(WORDS), val);
else
Reduce_Block(ser, index, val);
return R_TOS;
}
return R_ARG1;
}
The make prep build step always had the job of scanning the source code to connect the C function definitions with their matching native code.
But Ren-C's make prep step just goes the extra mile and extracts the native spec from comments.
This was done by @Brett many years ago in the beginning of Ren-C. ![]()
Now Let's Modernize Naming Conventions
![]()
// ...SPEC...
//
DECLARE_NATIVE(REDUCE)
{
if (Is_Block(ARG(VALUE))) {
Flex* flex = Series_Flex(ARG(VALUE));
Index index = Series_Index(ARG(VALUE));
Value* val = REF(INTO) ? ARG(OUT) : 0;
if (REF(NO_SET))
Reduce_Block_No_Set(flex, index, val);
else if (REF(ONLY))
Reduce_Only(flex, index, ARG(WORDS), val);
else
Reduce_Block(flex, index, val);
return R_TOS;
}
return R_ARG1;
}
-
C functions are
Words_Separated_By_Underscores(). At least one underscore appears in every function name. -
C datatypes are
CamelCasewith the reduced case ofTypeas a single word representing a datatype. -
Macros are sometimes ALL_CAPS and sometimes not, depending on if they are trying to look "function-like" (if they are function-like, they must not duplicate their arguments.)
An important naming choice was made to call the resizable data structure inside series cells a "Flex", and not try to call that a "Series" (or REBSER)
This way we know that a Series is a type of Cell... a composite of the data (a Flex...flexible memory abstraction) and an Index, along with a header. You can get your bearings a lot better this way.
e.g. here we see Series_Index(...) and know that it's asking a question of an ANY-SERIES Cell. We don't need to say Val_Series_Index(...) or Cell_Series_Index(...) to reinforce that it's a cell ... it has to be a cell to have an index.
But if we had a datatype called REBSER would you think you should be able to ask for the Rebser_Index(...)? This is why a choice like Flex is much stronger for the non-Cell stub, to separate it from what we think of as "a series".
Enforce Flex Subclasses In C++
![]()
// ...SPEC...
//
DECLARE_NATIVE(REDUCE)
{
if (Is_Block(ARG(VALUE))) {
Array* array = List_Array(ARG(VALUE)); // <-- Array* subclass of Flex
Index index = Series_Index(ARG(VALUE));
Value* val = REF(INTO) ? ARG(OUT) : 0;
if (REF(NO_SET))
Reduce_Block_No_Set(array, index, val);
else if (REF(ONLY))
Reduce_Only(array, index, ARG(WORDS), val);
else
Reduce_Block(array, index, val);
return R_TOS;
}
return R_ARG1;
}
It's extremely helpful to know if the Flex you are dealing with is the kind that holds Cells (an "Array"), or string data (a "Strand"), or bytes (a "Binary").
For instance, R3-Alpha's Reduce_Block() would have no idea what to do if you passed it a REBSER containing bytes for a BINARY!. It would just crash. There was no tool besides asserting at runtime that you got the unexpected type.
This is the fault of C. It doesn't have inheritance. If you wanted (Flex* Array* Strand* Binary*) which were type-incompatible in the compiler, they would have to point to completely different types...and the memory would be incompatible, unusable by common routines due to what is known as "Strict Aliasing".
Ren-C simply uses inheritance in C++, but not in C. e.g.
// 1. "Strand" holds UTF-8 bytes which are used by ANY-STRING types, but can
// be aliased as a BLOB!. So Strand Flexes inherit from Binary Flexes,
// and all operations legal on a Binary Flex are legal on Strands too.
//
#if CPLUSPLUS_11
struct Binary : public Flex {};
struct Strand : public Binary {}; // [1]
struct Array : public Flex {};
#else
typedef Flex Binary;
typedef Flex Strand;
typedef Flex Array;
#endif
Yes, it really is that simple. If you flip a switch and compile with C++ you would find places where you mistakenly tried to pass string memory to a place that was expecting cells.
This is the tip of the iceberg for why being able to compile a C codebase with C++ gives tremendous leverage. The benefits go exponential after that.
Ren-C isn't just about naming (!!!)
The transformations in Ren-C align the implementation with a vision of smaller, more composable pieces in the evaluator, enabling easier reasoning about natives and opening the door for richer, user-exposed control over evaluation.
But what I've shown so far above doesn't actually "do" anything different: you'd generate the same binary .EXE file on disk as before.
So I'll describe things that would affect the .EXE in a separate post.