*/ private $type2T = array(); /** @var array*/ private $T2descr = array(); private array $shared_tokens = array(); function __construct($config = array()) { $this->_initTables(); self::_addDefaultTokens($config); $this->config = $config; if(!isset($this->config['include_path'])) $this->config['include_path'] = array('.'); } private function _initTables() { $this->type2T = array( "string" => self::T_string, "uint32" => self::T_uint32, "int32" => self::T_int32, "uint16" => self::T_uint16, "int16" => self::T_int16, "uint8" => self::T_uint8, "int8" => self::T_int8, "float" => self::T_float, "double" => self::T_float, "uint64" => self::T_uint64, "int64" => self::T_int64, "bool" => self::T_bool, "blob" => self::T_blob, ); $this->T2descr = array_flip($this->type2T); $this->T2descr[self::T_EOF] = ''; $this->T2descr[self::T_StringConstant] = ''; $this->T2descr[self::T_RawStringConstant] = ''; $this->T2descr[self::T_IntegerConstant] = ''; $this->T2descr[self::T_FloatConstant] = ''; $this->T2descr[self::T_Enum] = ''; $this->T2descr[self::T_RPC] = ''; $this->T2descr[self::T_End] = ''; $this->T2descr[self::T_Identifier] = ''; $this->T2descr[self::T_Struct] = ''; $this->T2descr[self::T_Interface] = ''; $this->T2descr[self::T_Prop] = '<@prop>'; $this->T2descr[self::T_Extends] = ''; $this->T2descr[self::T_Implements] = ''; $this->T2descr[self::T_Func] = ''; } private static function _addDefaultTokens(array &$config) { if(!isset($config['valid_tokens'])) $config['valid_tokens'] = array(); $config['valid_tokens'][] = 'class_id'; $config['valid_tokens'][] = 'shared_tokens'; $config['valid_tokens'][] = 'enum_override'; $config['valid_tokens'][] = 'enum_replace'; } function parse(mtgMetaInfo $meta, $raw_file) { $this->current_meta = $meta; $file = realpath($raw_file); if($file === false) throw new Exception("No such file '$raw_file'"); $this->_parse($file); } private function _parse($file) { if(isset($this->parsed_files[$file])) return; $module = new mtgMetaParsedModule($file); $this->parsed_files[$file] = $module; $this->file_stack[] = $file; $source = file_get_contents($file); try { if($source === false) throw new Exception("Could not read file '$file'"); $this->_resolveIncludes($module, $source); } catch(Exception $e) { throw new Exception(end($this->file_stack) . " : " . $e->getMessage()); } array_pop($this->file_stack); $this->module = $module; $this->file = $file; $this->source = $source; $this->cursor = 0; $this->line = 1; $this->shared_tokens = array(); try { $this->_next(); while($this->T != self::T_EOF) { //echo "TOKEN : " . $this->T . " " . $this->T_value . " " . $this->line . "\n"; if($this->T == self::T_Prop) $this->_parseSharedTokens($this->_parsePropTokens()); else if($this->T == self::T_Enum) $this->_parseEnum(); else if($this->T == self::T_Struct) $this->_parseStruct(); else if($this->T == self::T_Interface) $this->_parseInterface(); else if($this->T == self::T_Func) $this->_parseFreeFunc(); else if($this->T == self::T_RPC) $this->_parseRPC(); else $this->_error("Unexpected T ('" . $this->_toStr($this->T) . "' " . $this->T_value . ")"); } } catch(Exception $e) { throw new Exception("$file@{$this->line} : " . $e->getMessage() . " " . $e->getTraceAsString()); } } private function _parseInclude(mtgMetaParsedModule $module, $file) { $this->_parse($file); $module->addInclude($this->parsed_files[$file]); } private function _parseSharedTokens(array $tokens) { if(!isset($tokens['shared_tokens'])) return; $this->shared_tokens = json_decode($tokens['shared_tokens'], true); if(!is_array($this->shared_tokens)) $this->_error("Invalid 'shared_tokens' formant, invalid JSON"); } private function _parseType($can_be_multi = false) { $types = array(); while(true) { $type = null; if($this->T == self::T_Func) { $origin = new mtgOrigin($this->file, $this->line); $func_type = $this->_parseFuncType(); $type = new mtgTypeRef($func_type, $this->module, $origin); } else if($this->T == self::T_Identifier) { $origin = new mtgOrigin($this->file, $this->line); $type_name = $this->_parseDotName(); $type = new mtgTypeRef($type_name, $this->module, $origin); } else { $origin = new mtgOrigin($this->file, $this->line); $type_name = $this->T_value; $type = new mtgTypeRef(new mtgBuiltinType($type_name), $this->module, $origin); $this->_next(); } if($this->T == ord('[')) { $origin = new mtgOrigin($this->file, $this->line); $this->_next(); $this->_checkThenNext(ord(']')); $type = new mtgTypeRef(new mtgArrType($type), $this->module, $origin); } $types[] = $type; if(!$can_be_multi) break; if($this->T != ord(',')) break; $this->_next(); } if(sizeof($types) > 1) return new mtgTypeRef(new mtgMultiType($types), $this->module, new mtgOrigin($this->file, $this->line)); else return $types[0]; } private function _parseFuncType() { $ftype = new mtgMetaFunc(''); $this->_next(); $this->_checkThenNext(ord('(')); $c = 0; while(true) { if($this->T == ord(')')) { $this->_next(); break; } else if($c > 0) { $this->_checkThenNext(ord(',')); } $arg_type = $this->_parseType(); $c++; $arg = new mtgMetaField("_$c", $arg_type); $ftype->addArg($arg); } if($this->T == ord(':')) { $this->_next(); $ret_type = $this->_parseType(true/*can be multi-type*/); $ftype->setReturnType($ret_type); } return $ftype; } private function _resolveIncludes(mtgMetaParsedModule $module, &$text) { $include_paths = $this->config['include_path']; $result = array(); $lines = explode("\n", $text); foreach($lines as $line) { if(preg_match('~^#include\s+(\S+)~', $line, $m)) { $this->_processInclude($module, $m[1], $include_paths); $result[] = ""; } else $result[] = $line; } $text = implode("\n", $result); } private function _processInclude(mtgMetaParsedModule $module, $include, array $include_paths) { $file = false; foreach($include_paths as $include_path) { $file = realpath($include_path . "/" . $include); if($file !== false) break; } if($file === false) throw new Exception("#include {$include} can't be resolved(include path is '". implode(':', $include_paths) . "')"); $this->_parseInclude($module, $file); } private function _parseEnumOrValues() { $values = array(); while(true) { if($this->T == self::T_Identifier) { $values[] = $this->T_value; $this->_next(); if(!$this->_nextIf(ord('|'))) break; } else break; } return $values; } private function _parseEnum() { $this->_next(); $name = $this->_parseDotName(); $enum = new mtgMetaEnum($name); $tokens = $this->shared_tokens; if($this->T == self::T_Prop) $tokens = array_merge($tokens, $this->_parsePropTokens()); $enum->setTokens($tokens); $or_values = array(); while(true) { if($this->_nextIf(self::T_End)) break; $key = $this->_checkThenNext(self::T_Identifier); $this->_checkThenNext(ord('=')); if($this->T == self::T_Identifier) { $or_values[$key] = $this->_parseEnumOrValues(); } else { $value = $this->_checkThenNext(self::T_IntegerConstant); $enum->addValue($key, $value); } } $enum->addOrValues($or_values); //NOTE: special case for enums when we allow to 'override' the original one, // with additional values if($enum->hasToken('enum_override')) { $existing = $this->current_meta->findUnit($enum->getMetaId()); if(!$existing) throw new Exception("Not found '{$name}' enum to override values"); if(!($existing->object instanceof mtgMetaEnum)) throw new Exception("Not an enum struct '{$name}'"); $existing->object->override($enum); } //NOTE: special case for enums when we allow to 'replace' the original one, // with additional values else if($enum->hasToken('enum_replace')) { $existing = $this->current_meta->findUnit($enum->getMetaId()); if(!$existing) throw new Exception("Not found '{$name}' enum to replace values"); if(!($existing->object instanceof mtgMetaEnum)) throw new Exception("Not an enum struct '{$name}'"); $existing->object->replace($enum); } else $this->_addUnit(new mtgMetaInfoUnit($this->file, $enum)); } static private function _isBuiltinType(int $t) : bool { return $t > self::T_MinType && $t < self::T_MaxType; } private function _parseFields(callable $next_doer) { $flds = array(); while(true) { if($next_doer()) break; if($this->T == self::T_Identifier) { $name = $this->T_value; $this->_next(); $this->_checkThenNext(ord(':')); if($this->T == self::T_Identifier || $this->T == self::T_Func || self::_isBuiltinType($this->T)) { $type = $this->_parseType(); $fld = new mtgMetaField($name, $type); if($this->T == self::T_Prop) $fld->setTokens($this->_parsePropTokens()); $flds[] = $fld; } else $this->_error("Type expected"); } else $this->_error("Unexpected fields T"); } return $flds; } private function _parseFuncs() { $end_token = self::T_End; $funcs = array(); while(true) { $fn = $this->_parseFunc(); $funcs[] = $fn; if($this->T == $end_token) { $this->_next(); break; } $this->_next(); } return $funcs; } private function _parseDotName() { $dot_name = ''; while(true) { if($this->T != self::T_Identifier) $this->_error("Unexpected name T"); $dot_name .= $this->T_value; $this->_next(); if($this->T != ord('.')) break; $dot_name .= '.'; $this->_next(); } return $dot_name; } private function _parseFunc() { $name = $this->_parseDotName(); $fn = new mtgMetaFunc($name); $this->_checkThenNext(ord('(')); if($this->T == self::T_Prop) $fn->setTokens($this->_parsePropTokens()); $args = $this->_parseFields(function() { return $this->_nextIf(ord(')')); } ); $fn->setArgs($args); $ret_type = null; if($this->T == ord(':')) { $this->_next(); if($this->T == self::T_Identifier || $this->T == self::T_Func || self::_isBuiltinType($this->T)) { $ret_type = $this->_parseType(true/*can be multi-type*/); $fn->setReturnType($ret_type); } else $this->_error("Unexpected func type"); } return $fn; } private function _addUnit(mtgMetaInfoUnit $unit) { $this->current_meta->addUnit($unit); $this->module->addUnit($unit); } private function _parseFreeFunc() { $this->_next(); $fn = $this->_parseFunc(); $fn->setTokens(array_merge($this->shared_tokens, $fn->getTokens())); $this->_addUnit(new mtgMetaInfoUnit($this->file, $fn)); } private function _parseStruct() { $this->_next(); $struct_origin = new mtgOrigin($this->file, $this->line); $name = $this->_parseDotName(); $parent = null; if($this->T == self::T_Extends) { $this->_next(); $origin = new mtgOrigin($this->file, $this->line); $parent_name = $this->_parseDotName(); $parent = new mtgTypeRef($parent_name, $this->module, $origin); } $implements = array(); if($this->T == self::T_Implements) { do { $this->_next(); $origin = new mtgOrigin($this->file, $this->line); $if_name = $this->_parseDotName(); $implements[] = new mtgTypeRef($if_name, $this->module, $origin); } while($this->T == ord(',')); } $s = new mtgMetaStruct($name, array(), $parent, array(), $implements); $s->setOrigin($struct_origin); $this->_addUnit(new mtgMetaInfoUnit($this->file, $s)); $tokens = $this->shared_tokens; if($this->T == self::T_Prop) $tokens = array_merge($tokens, $this->_parsePropTokens()); $s->setTokens($tokens); $seen_funcs = false; $flds = $this->_parseFields( function() use(&$seen_funcs) { if($this->_nextIf(self::T_End)) return true; if($this->_nextIf(self::T_Func)) { $seen_funcs = true; return true; } } ); foreach($flds as $fld) $s->addField($fld); if($seen_funcs) { $funcs = $this->_parseFuncs(); foreach($funcs as $fn) $s->addFunc($fn); } } private function _parseInterface() { $this->_next(); $name = $this->_parseDotName(); $s = new mtgMetaInterface($name); $this->_addUnit(new mtgMetaInfoUnit($this->file, $s)); $tokens = $this->shared_tokens; if($this->T == self::T_Prop) $tokens = array_merge($tokens, $this->_parsePropTokens()); $s->setTokens($tokens); if($this->T !== self::T_End) { $this->_next(); $funcs = $this->_parseFuncs(); foreach($funcs as $fn) $s->addFunc($fn); } else $this->_next(); } private function _parseRPC() { $this->_next(); $code = $this->_checkThenNext(self::T_IntegerConstant); $name = $this->_parseDotName(); $this->_checkThenNext(ord('(')); $tokens = $this->shared_tokens; if($this->T == self::T_Prop) $tokens = array_merge($tokens, $this->_parsePropTokens()); $req_fields = $this->_parseFields(function() { return $this->_nextIf(ord(')')); } ); $rsp_fields = $this->_parseFields(function() { return $this->_nextIf(self::T_End); } ); $req = new mtgMetaPacket($code, "RPC_REQ_$name"); $req->setFields($req_fields); $rsp = new mtgMetaPacket($code, "RPC_RSP_$name"); $rsp->setFields($rsp_fields); $rpc = new mtgMetaRPC("RPC_$name", $code, $req, $rsp, $tokens); $this->_addUnit(new mtgMetaInfoUnit($this->file, $rpc)); } private function _parsePropTokens() { $new_line = ord("\n"); $prop_tokens = array(); while(true) { if($this->T != self::T_Prop) break; $name = ltrim($this->T_value, '@'); $this->_validatePropToken($name); $this->_next(); $value = null; $value_start_line = $this->line; if($this->T == ord(':')) { while(true) { $this->_next(false/*don't skip new line*/); if($this->T == $new_line || $this->T == self::T_Prop) { //let's skip it if($this->T == $new_line) $this->_next(); break; } else { $tmp = $this->T_value; if($this->T == self::T_StringConstant) $tmp = "\"$tmp\""; if($value === null) $value = ''; $value .= $tmp; } } } if($value && substr($value, 0, 1) === '{') { $json = json_decode($value); if($json === null) { //for better line reporting $this->line = $value_start_line; $this->_error("Bad json"); } } $prop_tokens[$name] = $value; } return $prop_tokens; } private function _validatePropToken(string $name) { if(!isset($this->config['valid_tokens']) || !is_array($this->config['valid_tokens'])) return; if(!in_array($name, $this->config['valid_tokens'])) { throw new Exception("Unknown T '$name'"); } } private function _char() : string { $str = substr($this->source, $this->cursor, 1); if($str === false) $str = ''; return $str; } private function _next($skip_newlines = true) { while(true) { $c = $this->_char(); if($c == '') { $this->cursor--; $this->T = self::T_EOF; $this->T_value = $c; return; } $this->T = ord($c); $this->T_value = $c; ++$this->cursor; switch($c) { case ' ': case "\r": case "\t": break; case "\n": $this->line++; if($skip_newlines) break; else return; case '{': case '}': case '(': case ')': case '[': case ']': case '|': return; case ',': case ':': case ';': case '=': return; case '.': if(!ctype_digit($this->_char())) return; $this->_error("Floating point constant can't start with ."); break; case '"': $this->T_value = ""; while($this->_char() != '"') { if(ord($this->_char()) < ord(' ')) $this->_error("Illegal character in string constant"); if($this->_char() == '\\') { $this->cursor++; switch($this->_char()) { case 'n': $this->T_value .= "\n"; $this->cursor++; break; case 't': $this->T_value .= "\t"; $this->cursor++; break; case 'r': $this->T_value .= "\r"; $this->cursor++; break; case '"': $this->T_value .= '"'; $this->cursor++; break; case '\\': $this->T_value .= '\\'; $this->cursor++; break; default: $this->_error("Unknown escape code in string constant"); break; } } else // printable chars + UTF-8 bytes { $this->T_value .= $this->_char(); $this->cursor++; } } $this->T = self::T_StringConstant; $this->cursor++; return; case '`': $this->T_value = ""; while($this->_char() != '`') { $this->T_value .= $this->_char(); $this->cursor++; } $this->T = self::T_RawStringConstant; $this->cursor++; return; case '/': if($this->_char() == '/') { $this->cursor++; while($this->_char() != '' && $this->_char() != "\n") $this->cursor++; break; } case '#': while($this->_char() != '' && $this->_char() != "\n") $this->cursor++; break; case '@': $start = $this->cursor - 1; while(ctype_alnum($this->_char()) || $this->_char() == '_') $this->cursor++; $this->T = self::T_Prop; $this->T_value = substr($this->source, $start, $this->cursor - $start); return; //fall thru default: if(ctype_alpha($c)) { //collect all chars of an identifier $start = $this->cursor - 1; while(ctype_alnum($this->_char()) || $this->_char() == '_') $this->cursor++; $this->T_value = substr($this->source, $start, $this->cursor - $start); if(isset($this->type2T[$this->T_value])) { $this->T = $this->type2T[$this->T_value]; return; } if($this->T_value == "true" || $this->T_value == "false") { $this->T = self::T_IntegerConstant; return; } //check for declaration keywords: if($this->T_value == "struct") { $this->T = self::T_Struct; return; } if($this->T_value == "interface") { $this->T = self::T_Interface; return; } if($this->T_value == "enum") { $this->T = self::T_Enum; return; } if($this->T_value == "RPC") { $this->T = self::T_RPC; return; } if($this->T_value == "end") { $this->T = self::T_End; return; } if($this->T_value == "extends") { $this->T = self::T_Extends; return; } if($this->T_value == "implements") { $this->T = self::T_Implements; return; } if($this->T_value == "func") { $this->T = self::T_Func; return; } //if not it's a user defined identifier $this->T = self::T_Identifier; return; } else if(ctype_digit($c) || $c == '-') { $start = $this->cursor - 1; while(ctype_digit($this->_char())) $this->cursor++; if($this->_char() == '.') { $this->cursor++; while(ctype_digit($this->_char())) $this->cursor++; // see if this float has a scientific notation suffix. Both JSON // and C++ (through strtod() we use) have the same format: if($this->_char() == 'e' || $this->_char() == 'E') { $this->cursor++; if($this->_char() == '+' || $this->_char() == '-') $this->cursor++; while(ctype_digit($this->_char())) $this->cursor++; } $this->T = self::T_FloatConstant; } else $this->T = self::T_IntegerConstant; $this->T_value = substr($this->source, $start, $this->cursor - $start); return; } $this->_error("Illegal character '$c'"); } } } private function _nextIf(int $t) : bool { $yes = $t === $this->T; if($yes) $this->_next(); return $yes; } private function _checkThenNext(int $t) : string { if($t !== $this->T) { $this->_error("Expecting '" . $this->_toStr($t) . "' instead got '" . $this->_toStr($this->T) . "'"); } $attr = $this->T_value; $this->_next(); return $attr; } private function _toStr(int $t) : string { if($t < self::T_NonOrdMark) return chr($t); return $this->T2descr[$t]; } private function _error(string $msg) { throw new Exception($msg . " (T: {$this->T}, attr: {$this->T_value})"); } } class mtgMetaParsedModule { public $file; public $units = array(); public $includes = array(); function __construct($file) { $this->file = $file; } function addUnit(mtgMetaInfoUnit $unit) { $this->units[$unit->object->getMetaId()] = $unit; } function findUnit($id) { if(isset($this->units[$id])) return $this->units[$id]; foreach($this->includes as $include) { if(isset($include->units[$id])) return $include->units[$id]; } return null; } function addInclude(mtgMetaParsedModule $include) { $this->includes[] = $include; } } function mtg_parse_meta(array $meta_srcs, $valid_tokens = null, $inc_path = null) { if($inc_path === null) { //let's autodetect include path $inc_path = array(); foreach($meta_srcs as $src) { if(is_dir($src)) $inc_path[] = $src; else if(is_file($src)) $inc_path[] = dirname($src); } } $meta_parser = new mtgMetaInfoParser( array( 'include_path' => $inc_path, 'valid_tokens' => $valid_tokens ) ); $meta = new mtgMetaInfo(); foreach($meta_srcs as $src) mtg_load_meta($meta, $meta_parser, $src); $meta->validate(); mtgTypeRef::checkAllResolved(); return $meta; } function mtg_load_meta(mtgMetaInfo $meta, mtgMetaInfoParser $meta_parser, $dir_or_file) { $files = array(); if(is_dir($dir_or_file)) $files = mtg_find_meta_files($dir_or_file); else if(is_file($dir_or_file)) $files[] = $dir_or_file; else throw new Exception("Bad meta source '$dir_or_file'"); foreach($files as $file) $meta_parser->parse($meta, $file); } function mtg_find_meta_files($dir) { $items = scandir($dir); if($items === false) throw new Exception("Directory '$dir' is invalid"); $files = array(); foreach($items as $item) { if($item[0] == '.') continue; if(strpos($item, ".meta") !== (strlen($item)-5)) continue; $file = $dir . '/' . $item; if(is_file($file) && !is_dir($file)) $files[] = $file; } return $files; }