GDScript: support variable definitions in if-statement

This commit is contained in:
zjin 2024-10-19 13:05:39 +08:00
parent 44fa552343
commit 590a8cba02
24 changed files with 394 additions and 102 deletions

View file

@ -135,7 +135,16 @@ void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser
} break;
case GDScriptParser::Node::IF: {
const GDScriptParser::IfNode *if_node = static_cast<const GDScriptParser::IfNode *>(statement);
_assess_expression(if_node->condition);
const List<GDScriptParser::Node *>::Element *E = if_node->conditions.front();
while (E) {
const GDScriptParser::Node *node = E->get();
if (node->is_expression()) {
_assess_expression(static_cast<const GDScriptParser::ExpressionNode *>(node));
} else if (node->type == GDScriptParser::Node::VARIABLE) {
_assess_expression(static_cast<const GDScriptParser::VariableNode *>(node)->initializer);
}
E = E->next();
}
_traverse_block(if_node->true_block);
_traverse_block(if_node->false_block);
} break;

View file

@ -2096,7 +2096,16 @@ void GDScriptAnalyzer::resolve_parameter(GDScriptParser::ParameterNode *p_parame
}
void GDScriptAnalyzer::resolve_if(GDScriptParser::IfNode *p_if) {
reduce_expression(p_if->condition);
List<GDScriptParser::Node *>::Element *E = p_if->conditions.front();
while (E) {
GDScriptParser::Node *condition = E->get();
if (condition->is_expression()) {
reduce_expression(static_cast<GDScriptParser::ExpressionNode *>(condition));
} else if (condition->type == GDScriptParser::Node::VARIABLE) {
resolve_variable(static_cast<GDScriptParser::VariableNode *>(condition), true);
}
E = E->next();
}
resolve_suite(p_if->true_block);
p_if->set_datatype(p_if->true_block->get_datatype());

View file

@ -1501,13 +1501,16 @@ void GDScriptByteCodeGenerator::write_if(const Address &p_condition) {
append(0); // Jump destination, will be patched.
}
void GDScriptByteCodeGenerator::write_else() {
void GDScriptByteCodeGenerator::write_else(int count) {
append_opcode(GDScriptFunction::OPCODE_JUMP); // Jump from true if block;
int else_jmp_addr = opcodes.size();
append(0); // Jump destination, will be patched.
patch_jump(if_jmp_addrs.back()->get());
if_jmp_addrs.pop_back();
for (int i = 0; i < count; i++) {
patch_jump(if_jmp_addrs.back()->get());
if_jmp_addrs.pop_back();
}
if_jmp_addrs.push_back(else_jmp_addr);
}

View file

@ -532,7 +532,7 @@ public:
virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector<Address> &p_arguments) override;
virtual void write_await(const Address &p_target, const Address &p_operand) override;
virtual void write_if(const Address &p_condition) override;
virtual void write_else() override;
virtual void write_else(int count) override;
virtual void write_endif() override;
virtual void write_jump_if_shared(const Address &p_value) override;
virtual void write_end_jump_if_shared() override;

View file

@ -145,7 +145,7 @@ public:
virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector<Address> &p_arguments) = 0;
virtual void write_await(const Address &p_target, const Address &p_operand) = 0;
virtual void write_if(const Address &p_condition) = 0;
virtual void write_else() = 0;
virtual void write_else(int count) = 0;
virtual void write_endif() = 0;
virtual void write_jump_if_shared(const Address &p_value) = 0;
virtual void write_end_jump_if_shared() = 0;

View file

@ -251,6 +251,45 @@ static bool _can_use_validate_call(const MethodBind *p_method, const Vector<GDSc
return true;
}
Error GDScriptCompiler::_parse_variable(CodeGen &codegen, const GDScriptParser::VariableNode *p_variable, const GDScriptParser::SuiteNode *p_block) {
GDScriptCodeGenerator *gen = codegen.generator;
// Should be already in stack when the block began.
GDScriptCodeGenerator::Address local = codegen.locals[p_variable->identifier->name];
GDScriptDataType local_type = _gdtype_from_datatype(p_variable->get_datatype(), codegen.script);
Error err = OK;
bool initialized = false;
if (p_variable->initializer != nullptr) {
GDScriptCodeGenerator::Address src_address = _parse_expression(codegen, err, p_variable->initializer);
if (err) {
return err;
}
if (p_variable->use_conversion_assign) {
gen->write_assign_with_conversion(local, src_address);
} else {
gen->write_assign(local, src_address);
}
if (src_address.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
codegen.generator->pop_temporary();
}
initialized = true;
} else if ((local_type.has_type && local_type.kind == GDScriptDataType::BUILTIN) || codegen.generator->is_local_dirty(local)) {
// Initialize with default for the type. Built-in types must always be cleared (they cannot be `null`).
// Objects and untyped variables are assigned to `null` only if the stack address has been re-used and not cleared.
codegen.generator->clear_address(local);
initialized = true;
}
// Don't check `is_local_dirty()` since the variable must be assigned to `null` **on each iteration**.
if (!initialized && p_block->is_in_loop) {
codegen.generator->clear_address(local);
}
return err;
}
GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root, bool p_initializer) {
if (p_expression->is_constant && !(p_expression->get_datatype().is_meta_type && p_expression->get_datatype().kind == GDScriptParser::DataType::CLASS)) {
return codegen.add_constant(p_expression->reduced_value);
@ -1878,6 +1917,72 @@ void GDScriptCompiler::_clear_block_locals(CodeGen &codegen, const List<GDScript
}
}
Error GDScriptCompiler::_parse_if(CodeGen &codegen, const GDScriptParser::IfNode *if_n) {
Error err = OK;
GDScriptCodeGenerator *gen = codegen.generator;
List<GDScriptCodeGenerator::Address> block_locals;
gen->clear_temporaries();
codegen.start_block();
block_locals = _add_block_locals(codegen, if_n->condition_block);
int count = 0;
const List<GDScriptParser::Node *>::Element *E = if_n->conditions.front();
while (E) {
const GDScriptParser::Node *condition_n = E->get();
if (condition_n->is_expression()) {
const GDScriptParser::ExpressionNode *expression_n = static_cast<const GDScriptParser::ExpressionNode *>(condition_n);
GDScriptCodeGenerator::Address condition = _parse_expression(codegen, err, expression_n);
if (err) {
return err;
}
gen->write_if(condition);
if (condition.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
codegen.generator->pop_temporary();
}
count++;
} else if (condition_n->type == GDScriptParser::Node::VARIABLE) {
const GDScriptParser::VariableNode *variable_n = static_cast<const GDScriptParser::VariableNode *>(condition_n);
err = _parse_variable(codegen, variable_n, if_n->condition_block);
if (err) {
return err;
}
gen->clear_temporaries();
}
E = E->next();
}
err = _parse_block(codegen, if_n->true_block);
if (err) {
return err;
}
_clear_block_locals(codegen, block_locals);
codegen.end_block();
if (if_n->false_block) {
gen->write_else(count);
err = _parse_block(codegen, if_n->false_block);
if (err) {
return err;
}
gen->write_endif();
} else {
for (int i = 0; i < count; i++) {
gen->write_endif();
}
}
return OK;
}
Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, bool p_add_locals, bool p_clear_locals) {
Error err = OK;
GDScriptCodeGenerator *gen = codegen.generator;
@ -1935,7 +2040,7 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
for (int j = 0; j < match->branches.size(); j++) {
if (j > 0) {
// Use `else` to not check the next branch after matching.
gen->write_else();
gen->write_else(1);
}
const GDScriptParser::MatchBranchNode *branch = match->branches[j];
@ -2004,32 +2109,10 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
} break;
case GDScriptParser::Node::IF: {
const GDScriptParser::IfNode *if_n = static_cast<const GDScriptParser::IfNode *>(s);
GDScriptCodeGenerator::Address condition = _parse_expression(codegen, err, if_n->condition);
err = _parse_if(codegen, if_n);
if (err) {
return err;
}
gen->write_if(condition);
if (condition.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
codegen.generator->pop_temporary();
}
err = _parse_block(codegen, if_n->true_block);
if (err) {
return err;
}
if (if_n->false_block) {
gen->write_else();
err = _parse_block(codegen, if_n->false_block);
if (err) {
return err;
}
}
gen->write_endif();
} break;
case GDScriptParser::Node::FOR: {
const GDScriptParser::ForNode *for_n = static_cast<const GDScriptParser::ForNode *>(s);
@ -2166,36 +2249,10 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
#endif
} break;
case GDScriptParser::Node::VARIABLE: {
const GDScriptParser::VariableNode *lv = static_cast<const GDScriptParser::VariableNode *>(s);
// Should be already in stack when the block began.
GDScriptCodeGenerator::Address local = codegen.locals[lv->identifier->name];
GDScriptDataType local_type = _gdtype_from_datatype(lv->get_datatype(), codegen.script);
bool initialized = false;
if (lv->initializer != nullptr) {
GDScriptCodeGenerator::Address src_address = _parse_expression(codegen, err, lv->initializer);
if (err) {
return err;
}
if (lv->use_conversion_assign) {
gen->write_assign_with_conversion(local, src_address);
} else {
gen->write_assign(local, src_address);
}
if (src_address.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
codegen.generator->pop_temporary();
}
initialized = true;
} else if ((local_type.has_type && local_type.kind == GDScriptDataType::BUILTIN) || codegen.generator->is_local_dirty(local)) {
// Initialize with default for the type. Built-in types must always be cleared (they cannot be `null`).
// Objects and untyped variables are assigned to `null` only if the stack address has been re-used and not cleared.
codegen.generator->clear_address(local);
initialized = true;
}
// Don't check `is_local_dirty()` since the variable must be assigned to `null` **on each iteration**.
if (!initialized && p_block->is_in_loop) {
codegen.generator->clear_address(local);
const GDScriptParser::VariableNode *variable_n = static_cast<const GDScriptParser::VariableNode *>(s);
err = _parse_variable(codegen, variable_n, p_block);
if (err) {
return err;
}
} break;
case GDScriptParser::Node::CONSTANT: {

View file

@ -155,6 +155,8 @@ class GDScriptCompiler {
GDScriptCodeGenerator::Address _parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested);
List<GDScriptCodeGenerator::Address> _add_block_locals(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block);
void _clear_block_locals(CodeGen &codegen, const List<GDScriptCodeGenerator::Address> &p_locals);
Error _parse_variable(CodeGen &codegen, const GDScriptParser::VariableNode *p_variable, const GDScriptParser::SuiteNode *p_block);
Error _parse_if(CodeGen &codegen, const GDScriptParser::IfNode *if_n);
Error _parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, bool p_add_locals = true, bool p_clear_locals = true);
GDScriptFunction *_parse_function(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::FunctionNode *p_func, bool p_for_ready = false, bool p_for_lambda = false);
GDScriptFunction *_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class);

View file

@ -2205,25 +2205,33 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
}
}
if (suite->parent_if && suite->parent_if->condition && suite->parent_if->condition->type == GDScriptParser::Node::TYPE_TEST) {
// Operator `is` used, check if identifier is in there! this helps resolve in blocks that are (if (identifier is value)): which are very common..
// Super dirty hack, but very useful.
// Credit: Zylann.
// TODO: this could be hacked to detect ANDed conditions too...
const GDScriptParser::TypeTestNode *type_test = static_cast<const GDScriptParser::TypeTestNode *>(suite->parent_if->condition);
if (type_test->operand && type_test->test_type && type_test->operand->type == GDScriptParser::Node::IDENTIFIER && static_cast<const GDScriptParser::IdentifierNode *>(type_test->operand)->name == p_identifier->name && static_cast<const GDScriptParser::IdentifierNode *>(type_test->operand)->source == p_identifier->source) {
// Bingo.
GDScriptParser::CompletionContext c = p_context;
c.current_line = type_test->operand->start_line;
c.current_suite = suite;
if (type_test->test_datatype.is_hard_type()) {
id_type.type = type_test->test_datatype;
if (last_assign_line < c.current_line) {
// Override last assignment.
last_assign_line = c.current_line;
last_assigned_expression = nullptr;
if (suite->parent_if) {
List<GDScriptParser::Node *>::Element *E = suite->parent_if->conditions.front();
while (E) {
GDScriptParser::Node *condition = E->get();
if (condition->is_expression() && condition->type == GDScriptParser::Node::TYPE_TEST) {
// Operator `is` used, check if identifier is in there! this helps resolve in blocks that are (if (identifier is value)): which are very common..
// Super dirty hack, but very useful.
// Credit: Zylann.
// TODO: this could be hacked to detect ANDed conditions too...
const GDScriptParser::TypeTestNode *type_test = static_cast<const GDScriptParser::TypeTestNode *>(condition);
if (type_test->operand && type_test->test_type && type_test->operand->type == GDScriptParser::Node::IDENTIFIER && static_cast<const GDScriptParser::IdentifierNode *>(type_test->operand)->name == p_identifier->name && static_cast<const GDScriptParser::IdentifierNode *>(type_test->operand)->source == p_identifier->source) {
// Bingo.
GDScriptParser::CompletionContext c = p_context;
c.current_line = type_test->operand->start_line;
c.current_suite = suite;
if (type_test->test_datatype.is_hard_type()) {
id_type.type = type_test->test_datatype;
if (last_assign_line < c.current_line) {
// Override last assignment.
last_assign_line = c.current_line;
last_assigned_expression = nullptr;
}
}
break;
}
}
E = E->next();
}
}

View file

@ -1076,7 +1076,7 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) {
return parse_variable(p_is_static, true);
}
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property, bool p_is_if_condition) {
VariableNode *variable = alloc_node<VariableNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@ -1124,6 +1124,8 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, b
push_error(R"(Expected expression for variable initial value after "=".)");
}
variable->assignments++;
} else if (p_is_if_condition) {
push_error(R"(Expected expression for initial value after variable declaration.)");
}
if (p_allow_property && match(GDScriptTokenizer::Token::COLON)) {
@ -1135,7 +1137,10 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, b
}
complete_extents(variable);
end_statement("variable declaration");
if (p_is_if_condition == false) {
end_statement("variable declaration");
}
return variable;
}
@ -2085,21 +2090,70 @@ GDScriptParser::ForNode *GDScriptParser::parse_for() {
}
GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) {
IfNode *n_if = alloc_node<IfNode>();
SuiteNode *condition_block = alloc_node<SuiteNode>();
condition_block->parent_block = current_suite;
condition_block->parent_function = current_function;
SuiteNode *saved_suite = current_suite;
current_suite = condition_block;
n_if->condition = parse_expression(false);
if (n_if->condition == nullptr) {
push_error(vformat(R"(Expected conditional expression after "%s".)", p_token));
}
List<Node *> conditions;
do {
if (match(GDScriptTokenizer::Token::VAR)) {
// Variable declaration
VariableNode *variable = parse_variable(false, false, true);
if (variable == nullptr) {
push_error(vformat(R"(Expected variable definition after "%s".)", p_token));
break;
} else {
const SuiteNode::Local &local = current_suite->get_local(variable->identifier->name);
if (local.type != SuiteNode::Local::UNDEFINED) {
push_error(vformat(R"(There is already a %s named "%s" declared in this scope.)", local.get_name(), variable->identifier->name), variable->identifier);
break;
}
condition_block->add_local(variable, current_function);
IdentifierNode *identifier = alloc_node<IdentifierNode>();
identifier->name = variable->identifier->name;
identifier->suite = condition_block;
identifier->source = IdentifierNode::Source::LOCAL_VARIABLE;
const SuiteNode::Local &declaration = condition_block->get_local(identifier->name);
identifier->variable_source = declaration.variable;
declaration.variable->usages++;
complete_extents(identifier);
condition_block->statements.push_back(variable);
conditions.push_back(variable);
conditions.push_back(identifier);
}
} else {
// Expression
ExpressionNode *expression = parse_expression(false);
if (expression == nullptr) {
push_error(vformat(R"(Expected conditional expression after "%s".)", p_token));
break;
} else {
conditions.push_back(expression);
}
}
} while (match(GDScriptTokenizer::Token::COMMA));
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after "%s" condition.)", p_token));
n_if->true_block = parse_suite(vformat(R"("%s" block)", p_token));
n_if->true_block->parent_if = n_if;
SuiteNode *true_block = parse_suite(vformat(R"("%s" block)", p_token));
if (n_if->true_block->has_continue) {
current_suite->has_continue = true;
}
complete_extents(condition_block);
current_suite = saved_suite;
IfNode *n_if = alloc_node<IfNode>();
true_block->parent_function = current_function;
true_block->parent_block = condition_block;
true_block->parent_if = n_if;
n_if->conditions = conditions;
n_if->condition_block = condition_block;
n_if->true_block = true_block;
if (match(GDScriptTokenizer::Token::ELIF)) {
SuiteNode *else_block = alloc_node<SuiteNode>();
@ -2121,10 +2175,10 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) {
}
complete_extents(n_if);
if (n_if->false_block != nullptr && n_if->false_block->has_return && n_if->true_block->has_return) {
if ((n_if->false_block != nullptr && n_if->false_block->has_return) && n_if->true_block->has_return) {
current_suite->has_return = true;
}
if (n_if->false_block != nullptr && n_if->false_block->has_continue) {
if ((n_if->false_block != nullptr && n_if->false_block->has_continue) || n_if->true_block->has_continue) {
current_suite->has_continue = true;
}
@ -4785,11 +4839,19 @@ bool GDScriptParser::warning_annotations(AnnotationNode *p_annotation, Node *p_t
// Contain bodies.
SIMPLE_CASE(Node::FOR, ForNode, list)
SIMPLE_CASE(Node::IF, IfNode, condition)
SIMPLE_CASE(Node::MATCH, MatchNode, test)
SIMPLE_CASE(Node::WHILE, WhileNode, condition)
#undef SIMPLE_CASE
case Node::IF: {
IfNode *node = static_cast<IfNode *>(p_target);
if (node->conditions.is_empty()) {
end_line = node->start_line;
} else {
end_line = node->conditions.back()->get()->end_line;
}
} break;
case Node::CLASS: {
end_line = p_target->start_line;
for (const AnnotationNode *annotation : p_target->annotations) {
@ -5742,7 +5804,27 @@ void GDScriptParser::TreePrinter::print_if(IfNode *p_if, bool p_is_elif) {
} else {
push_text("If ");
}
print_expression(p_if->condition);
List<Node *>::Element *E = p_if->conditions.front();
bool first = true;
while (E) {
if (first) {
first = false;
} else {
push_text(", ");
}
Node *node = E->get();
if (node->is_expression()) {
print_expression(static_cast<ExpressionNode *>(node));
} else if (node->type == Node::VARIABLE) {
print_variable(static_cast<VariableNode *>(node));
// Skip next identifier condition
E = E->next();
} else {
ERR_PRINT("BUG: invalid condition");
}
E = E->next();
}
push_line(" :");
increase_indent();

View file

@ -918,7 +918,8 @@ public:
};
struct IfNode : public Node {
ExpressionNode *condition = nullptr;
List<Node *> conditions;
SuiteNode *condition_block = nullptr;
SuiteNode *true_block = nullptr;
SuiteNode *false_block = nullptr;
@ -1516,7 +1517,7 @@ private:
// Statements.
Node *parse_statement();
VariableNode *parse_variable(bool p_is_static);
VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
VariableNode *parse_variable(bool p_is_static, bool p_allow_property, bool p_is_if_condition = false);
VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
void parse_property_getter(VariableNode *p_variable);
void parse_property_setter(VariableNode *p_variable);

View file

@ -0,0 +1,5 @@
func test():
if var x = 100:
print("t")
elif x > 0:
print("f")

View file

@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Identifier "x" not declared in the current scope.

View file

@ -0,0 +1,5 @@
func test():
if var x = 100:
print("t")
else:
print(x)

View file

@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Identifier "x" not declared in the current scope.

View file

@ -0,0 +1,5 @@
func test():
if var x = 100:
print("t")
print(x)

View file

@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Identifier "x" not declared in the current scope.

View file

@ -1,4 +0,0 @@
func test():
# Error here.
if var foo = 25:
print(foo)

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected conditional expression after "if".

View file

@ -0,0 +1,3 @@
func test():
if var x := 1, var x := 2:
print("t")

View file

@ -0,0 +1,2 @@
GDTEST_PARSER_ERROR
There is already a variable named "x" declared in this scope.

View file

@ -0,0 +1,3 @@
func test():
if var x, true:
pass

View file

@ -0,0 +1,2 @@
GDTEST_PARSER_ERROR
Expected expression for initial value after variable declaration.

View file

@ -0,0 +1,53 @@
func foo(n: int) -> bool:
print("foo")
return n > 0
func bar(n: int) -> int:
print("bar")
return n
func add(a: int, b: int) -> int:
print("add")
return a + b
func test_if(f: bool, a: int, b: int) -> void:
print("--")
if f, foo(a), var n := bar(b):
print("t:%s" % n)
elif var n = add(a, b), n >= 1:
print("tt:%s" % n)
else:
print("f")
func test():
if var x = 25:
print(x)
if var x = 25:
print(x)
else:
print("ff")
if var x = 0:
print(x)
if var x = 0:
print(x)
else:
print("fff")
if var x := 100:
if x >= 1, var y := "ttt":
print(y)
print(100 + signi(x))
test_if(false, 0, 0);
test_if(false, 0, 1);
test_if(false, 1, 0);
test_if(false, 1, 1);
test_if(true, 0, 0);
test_if(true, 0, 1);
test_if(true, 0, 2);
test_if(true, 1, 0);
test_if(true, 1, 1);
test_if(true, 1, 2);

View file

@ -0,0 +1,43 @@
GDTEST_OK
25
25
fff
ttt
101
--
add
f
--
add
tt:1
--
add
tt:1
--
add
tt:2
--
foo
add
f
--
foo
add
tt:1
--
foo
add
tt:2
--
foo
bar
add
tt:1
--
foo
bar
t:1
--
foo
bar
t:2