/* ** Example: expressions intro ** ** 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. ** */ //~ includes and globals ////////////////////////////////////////////////////// #include "md.h" #include "md.c" 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 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. Node *node = expr->md_node; if ((node->flags & NodeFlag_MaskSetDelimiters) == 0) { fprintf(out, "%.*s", MD_S8VArg(node->raw_string)); } else { MD_CodeLoc loc = MD_CodeLocFromNode(node); MD_PrintMessage(stderr, loc, MD_MessageKind_Error, MD_S8Lit("the expression system does not expect this kind of node")); } } 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) { Node *node = expr->md_node; if (node->flags & NodeFlag_Numeric) { result = MD_CStyleIntFromString(node->string); } else if (node->flags & 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, "examples/expr/expr_intro.mdesk", }; argc = 2; argv = argv_dummy; #endif // setup the global arena arena = MD_ArenaAlloc(); // parse all files passed to the command line Node *list = MD_MakeList(arena); for (int i = 1; i < argc; i += 1) { // parse the file MD_String8 file_name = MD_S8CString(argv[i]); ParseResult parse_result = MD_ParseWholeFile(arena, file_name); // print metadesk errors for (MD_Message *message = parse_result.errors.first; message != 0; message = message->next) { MD_CodeLoc code_loc = MD_CodeLocFromNode(message->node); MD_PrintMessage(stdout, code_loc, message->kind, message->string); } // save to parse results list if (parse_result.errors.max_message_kind < MD_MessageKind_Error) { MD_PushNewReference(arena, list, parse_result.node); } } // setup the expression system MD_ExprOprTable table = {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_ExprBakeOprTableFromList(arena, &list); } // apply expression parsing to each top level node for (each_node(root_it, list->first_child)) { // init eval map eval_map = MD_MapMake(arena); Node *root = MD_ResolveNodeFromReference(root_it); for (each_node(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 for (MD_Message *message = parse.errors.first; message != 0; message = message->next) { MD_CodeLoc code_loc = MD_CodeLocFromNode(message->node); MD_PrintMessage(stdout, code_loc, message->kind, message->string); } if (parse.expr != 0) { // 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); } } } return 0; }