Skip to content

Commit 3e85182

Browse files
authored
Stack switching proposal support (#7041)
This patch implements text and binary encoding/decoding support for the stack switching proposal. It does so by adapting the previous typed-continunations implementation. Particular changes: * Support for new `resume` encoding. * Added support for `resume_throw` and `switch`. * Feature flag `typed-continuations` has been renamed to `stack-switching`. A small unfortunate implementation detail is that the internal name `Switch` was already taken by the `br_table` instruction, so I opted to give the `switch` instruction the internal name `StackSwitch`. A minor detail is that I have reordered the declarations/definitions of the stack switching instructions such that they appear in ascending order according to their opcode value (this is the same order that the stack-switching explainer document present them in). I can look into adding validation support in a subsequent patch.
1 parent 716e3c7 commit 3e85182

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2454
-463
lines changed

scripts/gen-s-parser.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,13 @@
596596
# Typed function references instructions
597597
("call_ref", "makeCallRef(/*isReturn=*/false)"),
598598
("return_call_ref", "makeCallRef(/*isReturn=*/true)"),
599-
# Typed continuations instructions
599+
# Stack switching instructions
600600
("cont.new", "makeContNew()"),
601601
("cont.bind", "makeContBind()"),
602-
("resume", "makeResume()"),
603602
("suspend", "makeSuspend()"),
603+
("resume", "makeResume()"),
604+
("resume_throw", "makeResumeThrow()"),
605+
("switch", "makeStackSwitch()"),
604606
# GC
605607
("ref.i31", "makeRefI31(Unshared)"),
606608
("ref.i31_shared", "makeRefI31(Shared)"),

scripts/test/fuzzing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@
9090
'names.wast',
9191
# huge amount of locals that make it extremely slow
9292
'too_much_for_liveness.wasm',
93+
# TODO: fuzzer support for stack switching
94+
'stack_switching.wast',
95+
'stack_switching_contnew.wast',
96+
'stack_switching_contbind.wast',
97+
'stack_switching_suspend.wast',
98+
'stack_switching_resume.wast',
99+
'stack_switching_resume_throw.wast',
100+
'stack_switching_switch.wast'
93101
]
94102

95103

src/gen-s-parser.inc

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4807,12 +4807,23 @@ switch (buf[0]) {
48074807
default: goto parse_error;
48084808
}
48094809
}
4810-
case 's':
4811-
if (op == "resume"sv) {
4812-
CHECK_ERR(makeResume(ctx, pos, annotations));
4813-
return Ok{};
4810+
case 's': {
4811+
switch (buf[6]) {
4812+
case '\0':
4813+
if (op == "resume"sv) {
4814+
CHECK_ERR(makeResume(ctx, pos, annotations));
4815+
return Ok{};
4816+
}
4817+
goto parse_error;
4818+
case '_':
4819+
if (op == "resume_throw"sv) {
4820+
CHECK_ERR(makeResumeThrow(ctx, pos, annotations));
4821+
return Ok{};
4822+
}
4823+
goto parse_error;
4824+
default: goto parse_error;
48144825
}
4815-
goto parse_error;
4826+
}
48164827
case 't': {
48174828
switch (buf[3]) {
48184829
case 'h':
@@ -5172,6 +5183,12 @@ switch (buf[0]) {
51725183
return Ok{};
51735184
}
51745185
goto parse_error;
5186+
case 'w':
5187+
if (op == "switch"sv) {
5188+
CHECK_ERR(makeStackSwitch(ctx, pos, annotations));
5189+
return Ok{};
5190+
}
5191+
goto parse_error;
51755192
default: goto parse_error;
51765193
}
51775194
}

src/interpreter/interpreter.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,12 @@ struct ExpressionInterpreter : OverriddenVisitor<ExpressionInterpreter, Flow> {
153153
Flow visitStringEq(StringEq* curr) { WASM_UNREACHABLE("TODO"); }
154154
Flow visitStringWTF16Get(StringWTF16Get* curr) { WASM_UNREACHABLE("TODO"); }
155155
Flow visitStringSliceWTF(StringSliceWTF* curr) { WASM_UNREACHABLE("TODO"); }
156-
Flow visitContBind(ContBind* curr) { WASM_UNREACHABLE("TODO"); }
157156
Flow visitContNew(ContNew* curr) { WASM_UNREACHABLE("TODO"); }
158-
Flow visitResume(Resume* curr) { WASM_UNREACHABLE("TODO"); }
157+
Flow visitContBind(ContBind* curr) { WASM_UNREACHABLE("TODO"); }
159158
Flow visitSuspend(Suspend* curr) { WASM_UNREACHABLE("TODO"); }
159+
Flow visitResume(Resume* curr) { WASM_UNREACHABLE("TODO"); }
160+
Flow visitResumeThrow(ResumeThrow* curr) { WASM_UNREACHABLE("TODO"); }
161+
Flow visitStackSwitch(StackSwitch* curr) { WASM_UNREACHABLE("TODO"); }
160162
};
161163

162164
} // anonymous namespace

src/ir/ReFinalize.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,10 @@ void ReFinalize::visitStringWTF16Get(StringWTF16Get* curr) { curr->finalize(); }
182182
void ReFinalize::visitStringSliceWTF(StringSliceWTF* curr) { curr->finalize(); }
183183
void ReFinalize::visitContNew(ContNew* curr) { curr->finalize(); }
184184
void ReFinalize::visitContBind(ContBind* curr) { curr->finalize(); }
185-
void ReFinalize::visitResume(Resume* curr) { curr->finalize(); }
186185
void ReFinalize::visitSuspend(Suspend* curr) { curr->finalize(getModule()); }
186+
void ReFinalize::visitResume(Resume* curr) { curr->finalize(); }
187+
void ReFinalize::visitResumeThrow(ResumeThrow* curr) { curr->finalize(); }
188+
void ReFinalize::visitStackSwitch(StackSwitch* curr) { curr->finalize(); }
187189

188190
void ReFinalize::visitExport(Export* curr) { WASM_UNREACHABLE("unimp"); }
189191
void ReFinalize::visitGlobal(Global* curr) { WASM_UNREACHABLE("unimp"); }

src/ir/branch-utils.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,14 @@ void operateOnScopeNameUsesAndSentTypes(Expression* expr, T func) {
8585
} else if (auto* r = expr->dynCast<Resume>()) {
8686
for (Index i = 0; i < r->handlerTags.size(); i++) {
8787
auto dest = r->handlerTags[i];
88-
if (dest == name) {
88+
if (!dest.isNull() && dest == name) {
89+
func(name, r->sentTypes[i]);
90+
}
91+
}
92+
} else if (auto* r = expr->dynCast<ResumeThrow>()) {
93+
for (Index i = 0; i < r->handlerTags.size(); i++) {
94+
auto dest = r->handlerTags[i];
95+
if (!dest.isNull() && dest == name) {
8996
func(name, r->sentTypes[i]);
9097
}
9198
}
@@ -118,6 +125,8 @@ void operateOnScopeNameUsesAndSentValues(Expression* expr, T func) {
118125
// The values are supplied by suspend instructions executed while running
119126
// the continuation, so we are unable to know what they will be here.
120127
func(name, nullptr);
128+
} else if (expr->is<ResumeThrow>()) {
129+
func(name, nullptr);
121130
} else {
122131
assert(expr->is<Try>() || expr->is<Rethrow>()); // delegate or rethrow
123132
}

src/ir/child-typer.h

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,39 +1085,76 @@ template<typename Subtype> struct ChildTyper : OverriddenVisitor<Subtype> {
10851085
note(&curr->end, Type::i32);
10861086
}
10871087

1088-
void visitContBind(ContBind* curr) {
1089-
auto paramsBefore =
1090-
curr->contTypeBefore.getContinuation().type.getSignature().params;
1091-
auto paramsAfter =
1092-
curr->contTypeAfter.getContinuation().type.getSignature().params;
1093-
assert(paramsBefore.size() >= paramsAfter.size());
1094-
auto n = paramsBefore.size() - paramsAfter.size();
1088+
void visitContNew(ContNew* curr) { note(&curr->func, curr->type); }
1089+
1090+
void visitContBind(ContBind* curr,
1091+
std::optional<HeapType> src = std::nullopt,
1092+
std::optional<HeapType> dest = std::nullopt) {
1093+
if (!src.has_value()) {
1094+
src = curr->cont->type.getHeapType();
1095+
}
1096+
if (!dest.has_value()) {
1097+
dest = curr->type.getHeapType();
1098+
}
1099+
auto sourceParams = src->getContinuation().type.getSignature().params;
1100+
auto targetParams = dest->getContinuation().type.getSignature().params;
1101+
assert(sourceParams.size() >= targetParams.size());
1102+
auto n = sourceParams.size() - targetParams.size();
10951103
assert(curr->operands.size() == n);
10961104
for (size_t i = 0; i < n; ++i) {
1097-
note(&curr->operands[i], paramsBefore[i]);
1105+
note(&curr->operands[i], sourceParams[i]);
10981106
}
1099-
note(&curr->cont, Type(curr->contTypeBefore, Nullable));
1107+
note(&curr->cont, Type(*src, Nullable));
11001108
}
11011109

1102-
void visitContNew(ContNew* curr) {
1103-
note(&curr->func, Type(curr->contType.getContinuation().type, Nullable));
1110+
void visitSuspend(Suspend* curr) {
1111+
auto params = wasm.getTag(curr->tag)->params();
1112+
assert(params.size() == curr->operands.size());
1113+
for (size_t i = 0; i < params.size(); ++i) {
1114+
note(&curr->operands[i], params[i]);
1115+
}
11041116
}
11051117

1106-
void visitResume(Resume* curr) {
1107-
auto params = curr->contType.getContinuation().type.getSignature().params;
1118+
void visitResume(Resume* curr, std::optional<HeapType> ct = std::nullopt) {
1119+
if (!ct.has_value()) {
1120+
ct = curr->cont->type.getHeapType();
1121+
}
1122+
assert(ct->isContinuation());
1123+
auto params = ct->getContinuation().type.getSignature().params;
11081124
assert(params.size() == curr->operands.size());
11091125
for (size_t i = 0; i < params.size(); ++i) {
11101126
note(&curr->operands[i], params[i]);
11111127
}
1112-
note(&curr->cont, Type(curr->contType, Nullable));
1128+
note(&curr->cont, Type(*ct, Nullable));
11131129
}
11141130

1115-
void visitSuspend(Suspend* curr) {
1131+
void visitResumeThrow(ResumeThrow* curr,
1132+
std::optional<HeapType> ct = std::nullopt) {
1133+
if (!ct.has_value()) {
1134+
ct = curr->cont->type.getHeapType();
1135+
}
1136+
assert(ct->isContinuation());
11161137
auto params = wasm.getTag(curr->tag)->params();
11171138
assert(params.size() == curr->operands.size());
11181139
for (size_t i = 0; i < params.size(); ++i) {
11191140
note(&curr->operands[i], params[i]);
11201141
}
1142+
note(&curr->cont, Type(*ct, Nullable));
1143+
}
1144+
1145+
void visitStackSwitch(StackSwitch* curr,
1146+
std::optional<HeapType> ct = std::nullopt) {
1147+
if (!ct.has_value()) {
1148+
ct = curr->cont->type.getHeapType();
1149+
}
1150+
assert(ct->isContinuation());
1151+
auto params = ct->getContinuation().type.getSignature().params;
1152+
assert(params.size() >= 1 &&
1153+
((params.size() - 1) == curr->operands.size()));
1154+
for (size_t i = 0; i < params.size() - 1; ++i) {
1155+
note(&curr->operands[i], params[i]);
1156+
}
1157+
note(&curr->cont, Type(*ct, Nullable));
11211158
}
11221159
};
11231160

src/ir/cost.h

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,10 @@ struct CostAnalyzer : public OverriddenVisitor<CostAnalyzer, CostType> {
764764
return 8 + visit(curr->ref) + visit(curr->start) + visit(curr->end);
765765
}
766766

767+
CostType visitContNew(ContNew* curr) {
768+
// Some arbitrary "high" value, reflecting that this may allocate a stack
769+
return 14 + visit(curr->func);
770+
}
767771
CostType visitContBind(ContBind* curr) {
768772
// Inspired by struct.new: The only cost of cont.bind is that it may need to
769773
// allocate a buffer to hold the arguments.
@@ -774,9 +778,12 @@ struct CostAnalyzer : public OverriddenVisitor<CostAnalyzer, CostType> {
774778
}
775779
return ret;
776780
}
777-
CostType visitContNew(ContNew* curr) {
778-
// Some arbitrary "high" value, reflecting that this may allocate a stack
779-
return 14 + visit(curr->func);
781+
CostType visitSuspend(Suspend* curr) {
782+
CostType ret = 12;
783+
for (auto* arg : curr->operands) {
784+
ret += visit(arg);
785+
}
786+
return ret;
780787
}
781788
CostType visitResume(Resume* curr) {
782789
// Inspired by indirect calls, but twice the cost.
@@ -786,8 +793,17 @@ struct CostAnalyzer : public OverriddenVisitor<CostAnalyzer, CostType> {
786793
}
787794
return ret;
788795
}
789-
CostType visitSuspend(Suspend* curr) {
790-
CostType ret = 12;
796+
CostType visitResumeThrow(ResumeThrow* curr) {
797+
// Inspired by indirect calls, but twice the cost.
798+
CostType ret = 12 + visit(curr->cont);
799+
for (auto* arg : curr->operands) {
800+
ret += visit(arg);
801+
}
802+
return ret;
803+
}
804+
CostType visitStackSwitch(StackSwitch* curr) {
805+
// Inspired by indirect calls, but twice the cost.
806+
CostType ret = 12 + visit(curr->cont);
791807
for (auto* arg : curr->operands) {
792808
ret += visit(arg);
793809
}

src/ir/effects.h

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,12 +1049,23 @@ class EffectAnalyzer {
10491049
// traps when ref is null.
10501050
parent.implicitTrap = true;
10511051
}
1052+
void visitContNew(ContNew* curr) {
1053+
// traps when curr->func is null ref.
1054+
parent.implicitTrap = true;
1055+
}
10521056
void visitContBind(ContBind* curr) {
10531057
// traps when curr->cont is null ref.
10541058
parent.implicitTrap = true;
10551059
}
1056-
void visitContNew(ContNew* curr) {
1057-
// traps when curr->func is null ref.
1060+
void visitSuspend(Suspend* curr) {
1061+
// Similar to resume/call: Suspending means that we execute arbitrary
1062+
// other code before we may resume here.
1063+
parent.calls = true;
1064+
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
1065+
parent.throws_ = true;
1066+
}
1067+
1068+
// A suspend may go unhandled and therefore trap.
10581069
parent.implicitTrap = true;
10591070
}
10601071
void visitResume(Resume* curr) {
@@ -1069,10 +1080,26 @@ class EffectAnalyzer {
10691080
parent.throws_ = true;
10701081
}
10711082
}
1072-
void visitSuspend(Suspend* curr) {
1073-
// Similar to resume/call: Suspending means that we execute arbitrary
1074-
// other code before we may resume here.
1083+
void visitResumeThrow(ResumeThrow* curr) {
1084+
// This acts as a kitchen sink effect.
10751085
parent.calls = true;
1086+
1087+
// resume_throw instructions accept nullable continuation
1088+
// references and trap on null.
1089+
parent.implicitTrap = true;
1090+
1091+
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
1092+
parent.throws_ = true;
1093+
}
1094+
}
1095+
void visitStackSwitch(StackSwitch* curr) {
1096+
// This acts as a kitchen sink effect.
1097+
parent.calls = true;
1098+
1099+
// switch instructions accept nullable continuation references
1100+
// and trap on null.
1101+
parent.implicitTrap = true;
1102+
10761103
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {
10771104
parent.throws_ = true;
10781105
}

src/ir/module-utils.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,19 @@ struct CodeScanner
448448
} else if (auto* set = curr->dynCast<ArraySet>()) {
449449
info.note(set->ref->type);
450450
} else if (auto* contBind = curr->dynCast<ContBind>()) {
451-
info.note(contBind->contTypeBefore);
452-
info.note(contBind->contTypeAfter);
451+
info.note(contBind->cont->type);
452+
info.note(contBind->type);
453453
} else if (auto* contNew = curr->dynCast<ContNew>()) {
454-
info.note(contNew->contType);
454+
info.note(contNew->type);
455455
} else if (auto* resume = curr->dynCast<Resume>()) {
456-
info.note(resume->contType);
456+
info.note(resume->cont->type);
457+
info.note(resume->type);
458+
} else if (auto* resumeThrow = curr->dynCast<ResumeThrow>()) {
459+
info.note(resumeThrow->cont->type);
460+
info.note(resumeThrow->type);
461+
} else if (auto* switch_ = curr->dynCast<StackSwitch>()) {
462+
info.note(switch_->cont->type);
463+
info.note(switch_->type);
457464
} else if (Properties::isControlFlowStructure(curr)) {
458465
info.noteControlFlow(Signature(Type::none, curr->type));
459466
}

0 commit comments

Comments
 (0)