From 07770d51d1b2e519b21b240109c903c9e0b0a0b0 Mon Sep 17 00:00:00 2001 From: Allen Webster Date: Sun, 10 Oct 2021 15:11:41 -0700 Subject: [PATCH] [examples] filled out the expression intro example and added commentary --- examples/expr/expr_intro.c | 161 +++++++++++++++++++++++++++++++-- examples/expr/expr_intro.mdesk | 27 +++++- source/md.h | 9 +- 3 files changed, 185 insertions(+), 12 deletions(-) diff --git a/examples/expr/expr_intro.c b/examples/expr/expr_intro.c index a214c1c..1979945 100644 --- a/examples/expr/expr_intro.c +++ b/examples/expr/expr_intro.c @@ -1,7 +1,9 @@ /* ** Example: expressions intro ** -** TODO +** This example shows how to use expression parsing in Metadesk. There is +** commentary about the setup as well as the features and limits of the +** expression parser. ** */ @@ -14,10 +16,14 @@ static MD_Arena *arena = 0; //~ expression setup and helpers ////////////////////////////////////////////// +// @notes A common easy setup is to give each operator a statically known +// integer code. We can switch on these integer codes later to interpret the +// operator nodes in expressions. enum { OpAdd, OpMul, + OpIllegal, }; void @@ -26,9 +32,16 @@ print_expression(FILE *out, MD_Expr *expr) MD_ExprOpr *op = expr->op; if (op == 0) { + // @notes Any MD_Expr that doesn't have an operator attached must be a + // leaf of the expression. In this example we don't want to recognize + // any nodes with set delimiters as leaves. + // + // Every expression node (MD_Expr) has an `md_node` regardless of + // whether it is a leaf or an operator. This node gives us a way to + // see information about this leaf, and also gives a way to create an + // MD_CodeLoc for error messages. The same works on operator nodes. MD_Node *node = expr->md_node; - if (node->raw_string.size != 0 && - MD_NodeIsNil(node->first_child)) + if ((node->flags & MD_NodeFlag_MaskSetDelimiters) == 0) { fprintf(out, "%.*s", MD_S8VArg(node->raw_string)); } @@ -41,19 +54,88 @@ print_expression(FILE *out, MD_Expr *expr) } else { + // @notes Any MD_Expr that does have an operator attached is an + // internal node of the expression. In this example we only setup + // binary operators, so we don't bother looking at what kind of + // operator this is. fprintf(out, "("); print_expression(out, expr->left); fprintf(out, " %.*s ", MD_S8VArg(op->string)); print_expression(out, expr->right); fprintf(out, ")"); + + if (op->op_id == OpIllegal) + { + MD_CodeLoc loc = MD_CodeLocFromNode(expr->md_node); + MD_PrintMessage(stderr, loc, MD_MessageKind_Error, + MD_S8Lit("this operator is not actually legal")); + } } } +// @notes Commonly a useful thing to do with an expression system is to +// evaluate the expressions. Here's a quick sketch of what that might look +// like. + +MD_Map eval_map = {0}; + +int +eval_expression(MD_Expr *expr) +{ + int result = 0; + MD_ExprOpr *op = expr->op; + if (op == 0) + { + MD_Node *node = expr->md_node; + if (node->flags & MD_NodeFlag_Numeric) + { + result = MD_CStyleIntFromString(node->string); + } + else if (node->flags & MD_NodeFlag_Identifier) + { + MD_MapSlot *slot = MD_MapLookup(&eval_map, MD_MapKeyStr(node->string)); + if (slot != 0) + { + result = (int)(MD_u64)slot->val; + } + } + } + else + { + int l = eval_expression(expr->left); + int r = eval_expression(expr->right); + + // @notes The `op_id` on this op pointer is carried to use from the + // operator setup where we used the OpAdd and OpMul enum to assign + // assign static integers to each operator. + switch (op->op_id) + { + case OpAdd: + { + result = l + r; + }break; + case OpMul: + { + result = l * r; + }break; + } + } + return(result); +} //~ main ////////////////////////////////////////////////////////////////////// int main(int argc, char **argv) { +#if 1 + char *argv_dummy[2] = { + 0, + "W:\\metadesk\\examples\\expr\\expr_intro.mdesk", + }; + argc = 2; + argv = argv_dummy; +#endif + // setup the global arena arena = MD_ArenaAlloc(); @@ -85,20 +167,72 @@ int main(int argc, char **argv) // setup the expression system MD_ExprOprTable table = {0}; { - MD_ExprOprList list = {0}; + // @notes To start using the expression system we have to decide what + // the expression operator table is going to look like. To do this we + // build up a list of operators and then bake that list into an + // optimized operator table for the parser to use. + // + // An operator string can be any string that parses as exactly one + // main node in metadesk. So it must count as a single token, and it + // cannot be a tag "@", set delimiter "()[]{}", or separator ",;". + // + // The system does have one special case for operator strings. A + // postfix operator may be created with "()", "[]", "{}", "[)" or "(]" + // these get specially interpreted to mean that a set with those + // delimiters may be used as a postfix operator. This can be used to + // parse things like array indexers and function calls. + // + // Here we just use symbol tokens as operators, but identifiers as + // operators are also allowed. + // + // Here we can attach user data in two forms (the last two parameters) + // The first is intended for static integers like enum values here. + // The second is intended for non-static user data like a pointer to + // another data structure. Both are optional. + // + // The bake function converts the list into a table optimized for + // parsing, but first it also checks the operator list. These checks + // include checking the names as described above, making sure + // there are no ambiguities from colliding operators, and making sure + // there are no mismatches of precedence levels and operator kinds + // that cannot be resolved. + // + // The memory used in the list is also used by the table, so they + // should be on the same arena, or at the very least the list's arena + // should not be cleared while the table is still in use. + MD_ExprOprList list = {0}; MD_ExprOprPush(arena, &list, MD_ExprOprKind_Binary, 1, MD_S8Lit("+"), OpAdd, 0); MD_ExprOprPush(arena, &list, MD_ExprOprKind_Binary, 2, MD_S8Lit("*"), OpMul, 0); + MD_ExprOprPush(arena, &list, MD_ExprOprKind_Binary, 3, MD_S8Lit("&"), OpIllegal, 0); table = MD_ExprBakeOperatorTableFromList(arena, &list); } - // print the verbose parse results + // apply expression parsing to each top level node for (MD_EachNode(root_it, list->first_child)) { + // init eval map + eval_map = MD_MapMake(arena); + MD_Node *root = MD_ResolveNodeFromReference(root_it); for (MD_EachNode(node, root->first_child)) { + // @notes An expression parse is an extra stage of analysis on top + // of the initial Metadesk parse. It takes in a range of Metadesk + // nodes specified as (first, one-past-last). Here we want to + // parse all of the children of the top-level node as a single + // expression, so we use the node's `first_child` as the first and + // nil as the one-past-last. + // + // The parser can return a list of new error messages, and an + // expression tree. + // + // The tree holds pointers back to the original operator data from + // the setup, so the tree should be on the same arena, or on at + // at least the operator memory should outlive the tree. + + // run the expression parse MD_ExprParseResult parse = MD_ExprParse(arena, &table, node->first_child, MD_NilNode()); // print errors @@ -110,13 +244,20 @@ int main(int argc, char **argv) MD_PrintMessage(stdout, code_loc, message->kind, message->string); } - // print the expression - if (node->string.size != 0) + if (parse.expr != 0) { - fprintf(stdout, "%.*s = ", MD_S8VArg(node->string)); + // evaluate the expression + int eval_result = eval_expression(parse.expr); + MD_MapInsert(arena, &eval_map, MD_MapKeyStr(node->string), (void*)(MD_u64)eval_result); + + // print the expression + if (node->string.size != 0) + { + fprintf(stdout, "%.*s = ", MD_S8VArg(node->string)); + } + print_expression(stdout, parse.expr); + fprintf(stdout, "; (%d)\n", eval_result); } - print_expression(stdout, parse.expr); - fprintf(stdout, ";\n"); } } diff --git a/examples/expr/expr_intro.mdesk b/examples/expr/expr_intro.mdesk index f8f4012..8933cbe 100644 --- a/examples/expr/expr_intro.mdesk +++ b/examples/expr/expr_intro.mdesk @@ -8,7 +8,32 @@ a: 1; b: 2; c: 3; -w: 100; +// @notes See expr_c_like.mdesk for an explanation of why these have to be +// wrapped in parentheses in some cases. +l: 5*5 + 1; +m: (5*(5 + 1)); +n: ((5 + 1)*(5 + 1)); + +w: 0x100; x: a; y: b + w*a; z: c + w*b + w*w*a; + +// @notes The Metadesk expression parser will automatically accept any set with +// delimiters as a leaf. In this example we are doing an extra custom check to +// disallow these cases. +range: ([0,64)); +origin: ({50,100}); +p: (origin + {0, -20}); + +// @notes We can also generate errors that point at operators just as easily: +`no_&`: 5000 & 0xFF; + +// @notes The expression parser will produce an error if the series of nodes +// that form the expression cannot form a complete expression. +bad_1: a b c; +bad_2: z % b; + +// @notes Surprising things might count as expressions! Anything can be a leaf +// so long as it is not recognized as an operator. +`good?`: ! + ? * ==; diff --git a/source/md.h b/source/md.h index 6b358d6..f2afa8b 100644 --- a/source/md.h +++ b/source/md.h @@ -561,21 +561,28 @@ enum MD_NodeFlag_HasBraceLeft = (1<<4), MD_NodeFlag_HasBraceRight = (1<<5), + MD_NodeFlag_MaskSetDelimiters = (0x3F<<0), + MD_NodeFlag_IsBeforeSemicolon = (1<<6), MD_NodeFlag_IsAfterSemicolon = (1<<7), - MD_NodeFlag_IsBeforeComma = (1<<8), MD_NodeFlag_IsAfterComma = (1<<9), + MD_NodeFlag_MaskSeperators = (0xF<<6), + MD_NodeFlag_StringSingleQuote = (1<<10), MD_NodeFlag_StringDoubleQuote = (1<<11), MD_NodeFlag_StringTick = (1<<12), MD_NodeFlag_StringTriplet = (1<<13), + MD_NodeFlag_MaskStringDelimiters = (0xF<<10), + MD_NodeFlag_Numeric = (1<<14), MD_NodeFlag_Identifier = (1<<15), MD_NodeFlag_StringLiteral = (1<<16), MD_NodeFlag_Symbol = (1<<17), + + MD_NodeFlag_MaskLabelKind = (0xF<<14), }; typedef struct MD_Node MD_Node;