mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 08:44:27 +00:00
[vm/compiler] Add support for more instructions to deserializer.
Adds support for: AllocateObject CheckNull LoadField StrictCompare Results from compiling hello world program: * Early round trip: * Contains unhandled instructions: 3065 * Failed during deserialization: 6 * Successful round trips: 1109 * Late round trip: * Contains unhandled instructions: 3085 * Failed during deserialization: 7 * Successful round trips: 1086 Bug: https://github.com/dart-lang/sdk/issues/36882 Change-Id: I09d25ca413a9facd27ba5a1b81e72a1fd126a3cc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/114860 Commit-Queue: Teagan Strickland <sstrickl@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
parent
7c8c197559
commit
4525105db9
8 changed files with 776 additions and 360 deletions
|
@ -1409,6 +1409,8 @@ class BlockEntryInstr : public Instruction {
|
|||
BitVector* block_marks);
|
||||
|
||||
private:
|
||||
friend class FlowGraphDeserializer; // Access to AddPredecessor().
|
||||
|
||||
virtual void RawSetInputAt(intptr_t i, Value* value) { UNREACHABLE(); }
|
||||
|
||||
virtual void ClearPredecessors() = 0;
|
||||
|
@ -3812,6 +3814,7 @@ class StrictCompareInstr : public TemplateComparison<2, NoThrow, Pure> {
|
|||
bool AttributesEqual(Instruction* other) const;
|
||||
|
||||
PRINT_OPERANDS_TO_SUPPORT
|
||||
ADD_EXTRA_INFO_TO_S_EXPRESSION_SUPPORT;
|
||||
|
||||
private:
|
||||
// True if the comparison must check for double or Mint and
|
||||
|
@ -7756,6 +7759,8 @@ class CheckNullInstr : public TemplateDefinition<1, Throws, Pure> {
|
|||
|
||||
virtual Value* RedefinedValue() const;
|
||||
|
||||
ADD_EXTRA_INFO_TO_S_EXPRESSION_SUPPORT
|
||||
|
||||
private:
|
||||
const TokenPosition token_pos_;
|
||||
const String& function_name_;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -48,9 +48,9 @@ class FlowGraphDeserializer : ValueObject {
|
|||
root_sexp_(ASSERT_NOTNULL(root)),
|
||||
parsed_function_(pf),
|
||||
block_map_(zone_),
|
||||
pushed_stack_map_(zone_),
|
||||
definition_map_(zone_),
|
||||
values_map_(zone_),
|
||||
pushed_stack_(zone_, 2),
|
||||
instance_class_(Class::Handle(zone)),
|
||||
instance_field_(Field::Handle(zone)),
|
||||
instance_object_(Object::Handle(zone)),
|
||||
|
@ -85,17 +85,21 @@ class FlowGraphDeserializer : ValueObject {
|
|||
M(TargetEntry)
|
||||
|
||||
#define FOR_EACH_HANDLED_INSTRUCTION_IN_DESERIALIZER(M) \
|
||||
M(AllocateObject) \
|
||||
M(Branch) \
|
||||
M(CheckNull) \
|
||||
M(CheckStackOverflow) \
|
||||
M(Constant) \
|
||||
M(DebugStepCheck) \
|
||||
M(Goto) \
|
||||
M(PushArgument) \
|
||||
M(LoadField) \
|
||||
M(Parameter) \
|
||||
M(PushArgument) \
|
||||
M(Return) \
|
||||
M(SpecialParameter) \
|
||||
M(StaticCall) \
|
||||
M(StoreInstanceField)
|
||||
M(StoreInstanceField) \
|
||||
M(StrictCompare)
|
||||
|
||||
// Helper methods for AllUnhandledInstructions.
|
||||
static bool IsHandledInstruction(Instruction* inst);
|
||||
|
@ -125,37 +129,38 @@ class FlowGraphDeserializer : ValueObject {
|
|||
bool ParseConstantPool(SExpList* pool);
|
||||
bool ParseEntries(SExpList* list);
|
||||
|
||||
struct CommonEntryInfo {
|
||||
intptr_t block_id;
|
||||
intptr_t try_index;
|
||||
intptr_t deopt_id;
|
||||
};
|
||||
using PushStack = ZoneGrowableArray<PushArgumentInstr*>;
|
||||
using BlockWorklist = GrowableArray<intptr_t>;
|
||||
|
||||
#define HANDLER_DECL(name) \
|
||||
name##Instr* Handle##name(SExpList* list, const CommonEntryInfo& info);
|
||||
// Starts parsing the contents of [list], where the blocks begin at position
|
||||
// [pos] and [worklist] contains the blocks whose body instructions should
|
||||
// be parsed first.
|
||||
bool ParseBlocks(SExpList* list, intptr_t pos, BlockWorklist* worklist);
|
||||
|
||||
FOR_EACH_HANDLED_BLOCK_TYPE_IN_DESERIALIZER(HANDLER_DECL);
|
||||
|
||||
#undef HANDLER_DECL
|
||||
|
||||
// Block parsing is split into two passes. This pass checks the
|
||||
// block ID and other extra information needed for certain block types.
|
||||
// In addition, it parses initial definitions found in the entry list.
|
||||
// The block is added to the [block_map_] as well as returned.
|
||||
BlockEntryInstr* ParseBlockHeader(SExpList* list, SExpSymbol* tag);
|
||||
// Block parsing is split into two passes. This pass adds function entries
|
||||
// to the flow graph and also parses initial definitions found in the Entries
|
||||
// list. The block is added to the [block_map_] before returning.
|
||||
BlockEntryInstr* ParseBlockHeader(SExpList* list,
|
||||
intptr_t block_id,
|
||||
SExpSymbol* tag);
|
||||
|
||||
// Expects [current_block_] to be set before calling.
|
||||
bool ParseInitialDefinitions(SExpList* list);
|
||||
|
||||
// Expects [current_block_] to be set before calling.
|
||||
// Takes the tagged list to parse and the index where parsing should start.
|
||||
// Returns the index of the first non-Phi instruction or definition or -1 on
|
||||
// error.
|
||||
intptr_t ParsePhis(SExpList* list, intptr_t pos);
|
||||
// Attempts to parse Phi definitions until the first non-Phi instruction.
|
||||
bool ParsePhis(SExpList* list);
|
||||
|
||||
// Parses the instructions in the body of a block. [current_block_] must be
|
||||
// set before calling.
|
||||
bool ParseBlockContents(SExpList* list);
|
||||
// Expects [current_block_] to be set before calling.
|
||||
// Returns the position of the first non-Phi instruction in a block.
|
||||
intptr_t SkipPhis(SExpList* list);
|
||||
|
||||
// Parses the deopt environment, Phi definitions for JoinEntrys, and the
|
||||
// instructions in the body of the block. Adds the IDs of the block successors
|
||||
// to the worklist, if any. [current_block_] and [pushed_stack_] must be set
|
||||
// before calling.
|
||||
bool ParseBlockContents(SExpList* list, BlockWorklist* worklist);
|
||||
|
||||
// Helper function used by ParseConstantPool, ParsePhis, and ParseDefinition.
|
||||
// This handles all the extra information stored in (def ...) expressions,
|
||||
|
@ -166,7 +171,20 @@ class FlowGraphDeserializer : ValueObject {
|
|||
Definition* ParseDefinition(SExpList* list);
|
||||
Instruction* ParseInstruction(SExpList* list);
|
||||
|
||||
struct CommonInstrInfo {
|
||||
struct EntryInfo {
|
||||
intptr_t block_id;
|
||||
intptr_t try_index;
|
||||
intptr_t deopt_id;
|
||||
};
|
||||
|
||||
#define HANDLER_DECL(name) \
|
||||
name##Instr* Deserialize##name(SExpList* list, const EntryInfo& info);
|
||||
|
||||
FOR_EACH_HANDLED_BLOCK_TYPE_IN_DESERIALIZER(HANDLER_DECL);
|
||||
|
||||
#undef HANDLER_DECL
|
||||
|
||||
struct InstrInfo {
|
||||
intptr_t deopt_id;
|
||||
TokenPosition token_pos;
|
||||
};
|
||||
|
@ -190,14 +208,25 @@ class FlowGraphDeserializer : ValueObject {
|
|||
#undef HANDLE_CASE
|
||||
|
||||
#define HANDLER_DECL(name) \
|
||||
name##Instr* Handle##name(SExpList* list, const CommonInstrInfo& info);
|
||||
name##Instr* Deserialize##name(SExpList* list, const InstrInfo& info);
|
||||
|
||||
FOR_EACH_HANDLED_INSTRUCTION_IN_DESERIALIZER(HANDLER_DECL);
|
||||
|
||||
#undef HANDLER_DECL
|
||||
|
||||
Value* ParseValue(SExpression* sexp);
|
||||
// Parses [sexp] as a value form, that is, either the binding name for
|
||||
// a definition as a symbol or the form (value <name> { ... }).
|
||||
// If [allow_pending], then values for definitions not already in the
|
||||
// [definition_map_] will be added to the [values_map_], otherwise,
|
||||
// values for definitions not yet seen cause an error to be stored and
|
||||
// nullptr to be returned.
|
||||
Value* ParseValue(SExpression* sexp, bool allow_pending = true);
|
||||
CompileType* ParseCompileType(SExpList* list);
|
||||
|
||||
// Parses [list] as an environment form: a list containing either binding
|
||||
// names for definitions or a# for pushed arguments (where # is the depth
|
||||
// of the argument from the top of the stack). Requires [pushed_stack_] to
|
||||
// be set if any references to pushed arguments are found.
|
||||
Environment* ParseEnvironment(SExpList* list);
|
||||
|
||||
// Parsing functions for which there are no good distinguished error
|
||||
|
@ -243,6 +272,14 @@ class FlowGraphDeserializer : ValueObject {
|
|||
// [block_map_]. Assumes all blocks have had their header parsed.
|
||||
BlockEntryInstr* FetchBlock(SExpSymbol* sym);
|
||||
|
||||
// Checks that the pushed argument stacks for all predecessors of [succ_block]
|
||||
// are the same as [curr_stack]. This check ensures that we can choose an
|
||||
// arbitrary predecessor's pushed argument stack when parsing [succ_block]'s
|
||||
// contents. [list] is used for error reporting.
|
||||
bool AreStacksConsistent(SExpList* list,
|
||||
PushStack* curr_stack,
|
||||
BlockEntryInstr* succ_block);
|
||||
|
||||
// Utility functions for checking the shape of an S-expression.
|
||||
// If these functions return nullptr for a non-null argument, they have the
|
||||
// side effect of setting the stored error message.
|
||||
|
@ -280,6 +317,12 @@ class FlowGraphDeserializer : ValueObject {
|
|||
// available via [flow_graph_].
|
||||
IntMap<BlockEntryInstr*> block_map_;
|
||||
|
||||
// Map from block IDs to pushed argument stacks. Used for PushArgument
|
||||
// instructions, environment parsing, and calls during block parsing. Also
|
||||
// used to check that the final pushed argument stacks for predecessor blocks
|
||||
// are consistent when parsing a JoinEntry.
|
||||
IntMap<PushStack*> pushed_stack_map_;
|
||||
|
||||
// Map from variable indexes to definitions.
|
||||
IntMap<Definition*> definition_map_;
|
||||
|
||||
|
@ -287,9 +330,6 @@ class FlowGraphDeserializer : ValueObject {
|
|||
// values that were parsed prior to the corresponding definition being found.
|
||||
IntMap<ZoneGrowableArray<Value*>*> values_map_;
|
||||
|
||||
// Stack of currently pushed arguments, used by environment parsing and calls.
|
||||
GrowableArray<PushArgumentInstr*> pushed_stack_;
|
||||
|
||||
// Temporary handles used by functions that are not re-entrant or where the
|
||||
// handle is not live after the re-entrant call. Comments show which handles
|
||||
// are expected to only be used within a single method.
|
||||
|
|
|
@ -490,8 +490,9 @@ SExpression* FlowGraphSerializer::FunctionToSExp(const Function& func) {
|
|||
AddExtraSymbol(sexp, "native_name", tmp_string_.ToCString());
|
||||
}
|
||||
}
|
||||
if (FLAG_verbose_flow_graph_serialization) {
|
||||
AddExtraSymbol(sexp, "kind", Function::KindToCString(func.kind()));
|
||||
if (func.kind() != RawFunction::Kind::kRegularFunction ||
|
||||
FLAG_verbose_flow_graph_serialization) {
|
||||
AddExtraSymbol(sexp, "kind", RawFunction::KindToCString(func.kind()));
|
||||
}
|
||||
function_type_args_ = func.type_parameters();
|
||||
if (auto const ta_sexp = NonEmptyTypeArgumentsToSExp(function_type_args_)) {
|
||||
|
@ -514,6 +515,10 @@ SExpression* FlowGraphSerializer::ArrayToSExp(const Array& arr) {
|
|||
array_elem = arr.At(i);
|
||||
sexp->Add(DartValueToSExp(array_elem));
|
||||
}
|
||||
array_type_args_ = arr.GetTypeArguments();
|
||||
if (auto const type_args_sexp = TypeArgumentsToSExp(array_type_args_)) {
|
||||
sexp->AddExtra("type_args", type_args_sexp);
|
||||
}
|
||||
return sexp;
|
||||
}
|
||||
|
||||
|
@ -840,6 +845,15 @@ void ComparisonInstr::AddOperandsToSExpression(SExpList* sexp,
|
|||
Instruction::AddOperandsToSExpression(sexp, s);
|
||||
}
|
||||
|
||||
void StrictCompareInstr::AddExtraInfoToSExpression(
|
||||
SExpList* sexp,
|
||||
FlowGraphSerializer* s) const {
|
||||
Instruction::AddExtraInfoToSExpression(sexp, s);
|
||||
if (needs_number_check_ || FLAG_verbose_flow_graph_serialization) {
|
||||
s->AddExtraBool(sexp, "needs_check", needs_number_check_);
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleTestOpInstr::AddOperandsToSExpression(SExpList* sexp,
|
||||
FlowGraphSerializer* s) const {
|
||||
const bool negated = kind() != Token::kEQ;
|
||||
|
@ -1081,6 +1095,14 @@ void CheckStackOverflowInstr::AddExtraInfoToSExpression(
|
|||
}
|
||||
}
|
||||
|
||||
void CheckNullInstr::AddExtraInfoToSExpression(SExpList* sexp,
|
||||
FlowGraphSerializer* s) const {
|
||||
Instruction::AddExtraInfoToSExpression(sexp, s);
|
||||
if (!function_name_.IsNull()) {
|
||||
s->AddExtraString(sexp, "function_name", function_name_.ToCString());
|
||||
}
|
||||
}
|
||||
|
||||
SExpression* Value::ToSExpression(FlowGraphSerializer* s) const {
|
||||
auto name = s->UseToSExp(definition());
|
||||
if (reaching_type_ == nullptr || reaching_type_ == definition()->type_ ||
|
||||
|
|
|
@ -111,6 +111,7 @@ class FlowGraphSerializer : ValueObject {
|
|||
: flow_graph_(ASSERT_NOTNULL(flow_graph)),
|
||||
zone_(zone),
|
||||
tmp_string_(String::Handle(zone_)),
|
||||
array_type_args_((TypeArguments::Handle(zone_))),
|
||||
closure_context_(Context::Handle(zone_)),
|
||||
closure_function_(Function::Handle(zone_)),
|
||||
closure_type_args_(TypeArguments::Handle(zone_)),
|
||||
|
@ -156,6 +157,7 @@ class FlowGraphSerializer : ValueObject {
|
|||
// DartValueToSExp with a sub-element of type Object, but any call to a
|
||||
// FlowGraphSerializer method that may eventually enter one of the methods
|
||||
// listed below should be examined with care.
|
||||
TypeArguments& array_type_args_; // ArrayToSExp
|
||||
Context& closure_context_; // ClosureToSExp
|
||||
Function& closure_function_; // ClosureToSExp
|
||||
TypeArguments& closure_type_args_; // ClosureToSExp
|
||||
|
|
|
@ -6211,66 +6211,7 @@ RawType* Function::RedirectionType() const {
|
|||
}
|
||||
|
||||
const char* Function::KindToCString(RawFunction::Kind kind) {
|
||||
switch (kind) {
|
||||
case RawFunction::kRegularFunction:
|
||||
return "RegularFunction";
|
||||
break;
|
||||
case RawFunction::kClosureFunction:
|
||||
return "ClosureFunction";
|
||||
break;
|
||||
case RawFunction::kImplicitClosureFunction:
|
||||
return "ImplicitClosureFunction";
|
||||
break;
|
||||
case RawFunction::kSignatureFunction:
|
||||
return "SignatureFunction";
|
||||
break;
|
||||
case RawFunction::kGetterFunction:
|
||||
return "GetterFunction";
|
||||
break;
|
||||
case RawFunction::kSetterFunction:
|
||||
return "SetterFunction";
|
||||
break;
|
||||
case RawFunction::kConstructor:
|
||||
return "Constructor";
|
||||
break;
|
||||
case RawFunction::kImplicitGetter:
|
||||
return "ImplicitGetter";
|
||||
break;
|
||||
case RawFunction::kImplicitSetter:
|
||||
return "ImplicitSetter";
|
||||
break;
|
||||
case RawFunction::kImplicitStaticGetter:
|
||||
return "ImplicitStaticGetter";
|
||||
break;
|
||||
case RawFunction::kFieldInitializer:
|
||||
return "FieldInitializer";
|
||||
break;
|
||||
case RawFunction::kMethodExtractor:
|
||||
return "MethodExtractor";
|
||||
break;
|
||||
case RawFunction::kNoSuchMethodDispatcher:
|
||||
return "NoSuchMethodDispatcher";
|
||||
break;
|
||||
case RawFunction::kInvokeFieldDispatcher:
|
||||
return "InvokeFieldDispatcher";
|
||||
break;
|
||||
case RawFunction::kIrregexpFunction:
|
||||
return "IrregexpFunction";
|
||||
break;
|
||||
case RawFunction::kDynamicInvocationForwarder:
|
||||
return "DynamicInvocationForwarder";
|
||||
break;
|
||||
case RawFunction::kFfiTrampoline:
|
||||
return "FfiTrampoline";
|
||||
break;
|
||||
}
|
||||
// When you add a case to this switch, please also update the observatory.
|
||||
// - runtime/observatory/lib/src/models/objects/function.dart (FunctionKind)
|
||||
// - runtime/observatory/lib/src/elements/function_view.dart
|
||||
// (_functionKindToString)
|
||||
// - runtime/observatory/lib/src/service/object.dart (stringToFunctionKind)
|
||||
UNREACHABLE();
|
||||
return NULL;
|
||||
return RawFunction::KindToCString(kind);
|
||||
}
|
||||
|
||||
void Function::SetRedirectionType(const Type& type) const {
|
||||
|
|
|
@ -838,33 +838,78 @@ class RawPatchClass : public RawObject {
|
|||
|
||||
class RawFunction : public RawObject {
|
||||
public:
|
||||
// When you add a new kind, please also update the observatory to account
|
||||
// for the new string returned by KindToCString().
|
||||
// - runtime/observatory/lib/src/models/objects/function.dart (FunctionKind)
|
||||
// - runtime/observatory/lib/src/elements/function_view.dart
|
||||
// (_functionKindToString)
|
||||
// - runtime/observatory/lib/src/service/object.dart (stringToFunctionKind)
|
||||
#define FOR_EACH_RAW_FUNCTION_KIND(V) \
|
||||
/* an ordinary or operator method */ \
|
||||
V(RegularFunction) \
|
||||
/* a user-declared closure function */ \
|
||||
V(ClosureFunction) \
|
||||
/* an implicit closure (i.e., tear-off) */ \
|
||||
V(ImplicitClosureFunction) \
|
||||
/* a signature only without actual code */ \
|
||||
V(SignatureFunction) \
|
||||
/* getter functions e.g: get foo() { .. } */ \
|
||||
V(GetterFunction) \
|
||||
/* setter functions e.g: set foo(..) { .. } */ \
|
||||
V(SetterFunction) \
|
||||
/* a generative (is_static=false) or factory (is_static=true) constructor */ \
|
||||
V(Constructor) \
|
||||
/* an implicit getter for instance fields */ \
|
||||
V(ImplicitGetter) \
|
||||
/* an implicit setter for instance fields */ \
|
||||
V(ImplicitSetter) \
|
||||
/* represents an implicit getter for static fields with initializers */ \
|
||||
V(ImplicitStaticGetter) \
|
||||
/* the initialization expression for a static or instance field */ \
|
||||
V(FieldInitializer) \
|
||||
/* return a closure on the receiver for tear-offs */ \
|
||||
V(MethodExtractor) \
|
||||
/* builds an Invocation and invokes noSuchMethod */ \
|
||||
V(NoSuchMethodDispatcher) \
|
||||
/* invokes a field as a closure (i.e., call-through-getter) */ \
|
||||
V(InvokeFieldDispatcher) \
|
||||
/* a generated irregexp matcher function. */ \
|
||||
V(IrregexpFunction) \
|
||||
/* a forwarder which performs type checks for arguments of a dynamic call */ \
|
||||
/* (i.e., those checks omitted by the caller for interface calls). */ \
|
||||
V(DynamicInvocationForwarder) \
|
||||
V(FfiTrampoline)
|
||||
|
||||
enum Kind {
|
||||
kRegularFunction, // an ordinary or operator method
|
||||
kClosureFunction, // a user-declared closure function
|
||||
kImplicitClosureFunction, // an implicit closure (i.e., tear-off)
|
||||
kSignatureFunction, // a signature only without actual code
|
||||
kGetterFunction, // getter functions e.g: get foo() { .. }
|
||||
kSetterFunction, // setter functions e.g: set foo(..) { .. }
|
||||
kConstructor, // a generative (is_static=false) or
|
||||
// factory (is_static=true) constructor
|
||||
kImplicitGetter, // an implicit getter for instance fields
|
||||
kImplicitSetter, // an implicit setter for instance fields
|
||||
kImplicitStaticGetter, // represents an implicit getter for static
|
||||
// fields with initializers
|
||||
kFieldInitializer, // the initialization expression for a static
|
||||
// or instance field
|
||||
kMethodExtractor, // return a closure on the receiver for tear-offs
|
||||
kNoSuchMethodDispatcher, // builds an Invocation and invokes noSuchMethod
|
||||
kInvokeFieldDispatcher, // invokes a field as a closure (i.e.,
|
||||
// call-through-getter)
|
||||
kIrregexpFunction, // a generated irregexp matcher function.
|
||||
kDynamicInvocationForwarder, // a forwarder which performs type checks for
|
||||
// arguments of a dynamic call (i.e., those
|
||||
// checks omitted by the caller for interface
|
||||
// calls).
|
||||
kFfiTrampoline,
|
||||
#define KIND_DEFN(Name) k##Name,
|
||||
FOR_EACH_RAW_FUNCTION_KIND(KIND_DEFN)
|
||||
#undef KIND_DEFN
|
||||
};
|
||||
|
||||
static const char* KindToCString(Kind k) {
|
||||
switch (k) {
|
||||
#define KIND_CASE(Name) \
|
||||
case Kind::k##Name: \
|
||||
return #Name;
|
||||
FOR_EACH_RAW_FUNCTION_KIND(KIND_CASE)
|
||||
#undef KIND_CASE
|
||||
default:
|
||||
UNREACHABLE();
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static bool KindFromCString(const char* str, Kind* out) {
|
||||
#define KIND_CASE(Name) \
|
||||
if (strcmp(str, #Name) == 0) { \
|
||||
*out = Kind::k##Name; \
|
||||
return true; \
|
||||
}
|
||||
FOR_EACH_RAW_FUNCTION_KIND(KIND_CASE)
|
||||
#undef KIND_CASE
|
||||
return false;
|
||||
}
|
||||
|
||||
enum AsyncModifier {
|
||||
kNoModifier = 0x0,
|
||||
kAsyncBit = 0x1,
|
||||
|
|
|
@ -259,6 +259,19 @@ class Token {
|
|||
return tok_str_[tok];
|
||||
}
|
||||
|
||||
static bool FromStr(const char* str, Kind* out) {
|
||||
ASSERT(str != nullptr && out != nullptr);
|
||||
#define TOK_CASE(t, s, p, a) \
|
||||
if (strcmp(str, tok_str_[(t)]) == 0) { \
|
||||
*out = (t); \
|
||||
return true; \
|
||||
}
|
||||
DART_TOKEN_LIST(TOK_CASE)
|
||||
DART_KEYWORD_LIST(TOK_CASE)
|
||||
#undef TOK_CASE
|
||||
return false;
|
||||
}
|
||||
|
||||
static int Precedence(Kind tok) {
|
||||
ASSERT(tok < kNumTokens);
|
||||
return precedence_[tok];
|
||||
|
|
Loading…
Reference in a new issue