aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/WebAssembly/Validation
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-29 18:23:59 +0900
committernsfisis <nsfisis@gmail.com>2025-10-18 19:47:13 +0900
commita0ec6fc11cbb68d5912e850b9f73b94f4560d510 (patch)
tree070ab472bb1bd3c5bb739b2b19052d1083fe9acd /src/WebAssembly/Validation
parent2656a7d724c2738bd8fa8d3a876a1228642bf54e (diff)
downloadphp-waddiwasi-a0ec6fc11cbb68d5912e850b9f73b94f4560d510.tar.gz
php-waddiwasi-a0ec6fc11cbb68d5912e850b9f73b94f4560d510.tar.zst
php-waddiwasi-a0ec6fc11cbb68d5912e850b9f73b94f4560d510.zip
Diffstat (limited to 'src/WebAssembly/Validation')
-rw-r--r--src/WebAssembly/Validation/Context.php42
-rw-r--r--src/WebAssembly/Validation/ControlFrame.php23
-rw-r--r--src/WebAssembly/Validation/ValidationFailureException.php11
-rw-r--r--src/WebAssembly/Validation/ValidationResult.php16
-rw-r--r--src/WebAssembly/Validation/Validator.php974
5 files changed, 1066 insertions, 0 deletions
diff --git a/src/WebAssembly/Validation/Context.php b/src/WebAssembly/Validation/Context.php
new file mode 100644
index 0000000..59c7db4
--- /dev/null
+++ b/src/WebAssembly/Validation/Context.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\WebAssembly\Validation;
+
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\FuncType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\GlobalType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\MemType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\TableType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType;
+
+final class Context
+{
+ /**
+ * @param list<FuncType> $types
+ * @param list<FuncType> $funcs
+ * @param list<TableType> $tables
+ * @param list<MemType> $mems
+ * @param list<GlobalType> $globals
+ * @param list<ValType> $elems
+ * @param list<bool> $datas
+ * @param list<mixed> $locals
+ * @param list<mixed> $labels
+ * @param list<mixed> $return
+ * @param list<int> $refs
+ */
+ public function __construct(
+ public array $types,
+ public array $funcs,
+ public array $tables,
+ public array $mems,
+ public array $globals,
+ public array $elems,
+ public array $datas,
+ public array $locals,
+ public array $labels,
+ public array $return,
+ public array $refs,
+ ) {
+ }
+}
diff --git a/src/WebAssembly/Validation/ControlFrame.php b/src/WebAssembly/Validation/ControlFrame.php
new file mode 100644
index 0000000..4decc1e
--- /dev/null
+++ b/src/WebAssembly/Validation/ControlFrame.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\WebAssembly\Validation;
+
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType;
+
+final readonly class ControlFrame
+{
+ /**
+ * @param list<ValType> $startTypes
+ * @param list<ValType> $endTypes
+ */
+ public function __construct(
+ public string $opcode,
+ public array $startTypes,
+ public array $endTypes,
+ public int $height,
+ public bool $unreachable,
+ ) {
+ }
+}
diff --git a/src/WebAssembly/Validation/ValidationFailureException.php b/src/WebAssembly/Validation/ValidationFailureException.php
new file mode 100644
index 0000000..9650e80
--- /dev/null
+++ b/src/WebAssembly/Validation/ValidationFailureException.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\WebAssembly\Validation;
+
+use RuntimeException;
+
+final class ValidationFailureException extends RuntimeException
+{
+}
diff --git a/src/WebAssembly/Validation/ValidationResult.php b/src/WebAssembly/Validation/ValidationResult.php
new file mode 100644
index 0000000..0a9395a
--- /dev/null
+++ b/src/WebAssembly/Validation/ValidationResult.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\WebAssembly\Validation;
+
+final readonly class ValidationResult
+{
+ /**
+ * @param list<string> $errors
+ */
+ public function __construct(
+ public array $errors,
+ ) {
+ }
+}
diff --git a/src/WebAssembly/Validation/Validator.php b/src/WebAssembly/Validation/Validator.php
new file mode 100644
index 0000000..6c11063
--- /dev/null
+++ b/src/WebAssembly/Validation/Validator.php
@@ -0,0 +1,974 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\WebAssembly\Validation;
+
+use Nsfisis\Waddiwasi\WebAssembly\Execution\ExternVal;
+use Nsfisis\Waddiwasi\WebAssembly\Execution\ExternVals;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instr;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instrs;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instrs\Control\BlockType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instrs\Control\BlockTypes;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\Func;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\Module;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\FuncType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\Limits;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\MemType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\TableType;
+use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType;
+use function array_slice;
+use function count;
+
+final class Validator
+{
+ /**
+ * @var list<string>
+ */
+ private array $errors = [];
+
+ /**
+ * @var list<?ValType>
+ */
+ private array $valueStack = [];
+
+ /**
+ * @var list<ControlFrame>
+ */
+ private array $controlStack = [];
+
+ /**
+ * @param list<ExternVal> $externVals
+ */
+ public function __construct(
+ private readonly Module $module,
+ private readonly array $externVals,
+ ) {
+ }
+
+ public function validate(): ValidationResult
+ {
+ try {
+ return new ValidationResult($this->errors);
+ } catch (ValidationFailureException $e) {
+ return new ValidationResult([...$this->errors, $e->getMessage()]);
+ }
+ }
+
+ private function validateModule(): void
+ {
+ $c = new Context(
+ types: $this->module->types,
+ funcs: array_merge(
+ array_filter($this->externVals, fn ($v) => $v instanceof ExternVals\Func),
+ array_map(fn ($f) => $f->type, $this->module->funcs),
+ ),
+ tables: array_merge(
+ array_filter($this->externVals, fn ($v) => $v instanceof ExternVals\Table),
+ array_map(fn ($t) => $t->type, $this->module->tables),
+ ),
+ mems: array_merge(
+ array_filter($this->externVals, fn ($v) => $v instanceof ExternVals\Mem),
+ array_map(fn ($m) => $m->type, $this->module->mems),
+ ),
+ globals: array_merge(
+ array_filter($this->externVals, fn ($v) => $v instanceof ExternVals\Global_),
+ array_map(fn ($g) => $g->type, $this->module->globals),
+ ),
+ elems: array_map(fn ($e) => $e->type, $this->module->elems),
+ datas: TODO,
+ locals: [],
+ labels: [],
+ return: [],
+ refs: TODO,
+ );
+ $c_ = clone $c;
+ $c_->globals = array_filter($this->externVals, fn ($v) => $v instanceof ExternVals\Global_);
+
+ $this->validateTypes();
+
+ $this->validateTables($c_);
+ $this->validateMems($c_);
+ $this->validateGlobals($c_);
+ $this->validateElems($c_);
+ $this->validateDatas($c_);
+
+ $this->validateFuncs();
+ $this->validateStart();
+ $this->validateImports();
+ $this->validateExports();
+
+ $this->validateMemsCount();
+ $this->validateExportsHaveDifferentNames();
+ }
+
+ private function validateTypes(): void
+ {
+ // Do nothing because function types are always valid.
+ }
+
+ private function validateFuncs(Context $c): void
+ {
+ foreach ($this->module->funcs as $func) {
+ $this->validateFunc($func);
+ }
+ }
+
+ private function validateTables(Context $c): void
+ {
+ foreach ($this->module->tables as $i => $table) {
+ $this->validateTableType($table->type);
+ }
+ }
+
+ private function validateTableType(TableType $tableType): void
+ {
+ $this->validateLimits($tableType->limits, 2 ** 32 - 1);
+ }
+
+ private function validateLimits(Limits $limits, int $k): void
+ {
+ $n = $limits->min;
+ $m = $limits->max;
+ $this->assertTrue($n <= $k, 'invalid limits');
+ if ($m !== null) {
+ $this->assertTrue($m <= $k, 'invalid limits');
+ $this->assertTrue($n <= $m, 'invalid limits');
+ }
+ }
+
+ private function validateMems(Context $c): void
+ {
+ foreach ($this->module->mems as $i => $mem) {
+ $this->validateMemType($mem->type);
+ }
+ }
+
+ private function validateMemType(MemType $memType): void
+ {
+ $this->validateLimits($memType->limits, 2 ** 16);
+ }
+
+ private function validateGlobals(Context $c): void
+ {
+ foreach ($this->module->globals as $i => $global) {
+ $this->validateConstantExpr($global->init, $global->type->valType, $c);
+ }
+ }
+
+ /**
+ * @param list<Instr> $instrs
+ */
+ private function validateConstantExpr(array $instrs, ValType $expectedType, Context $c): void
+ {
+ $this->assertTrue(count($instrs) === 1, 'expr is not constant');
+ $instr = $instrs[0] ?? null;
+ if ($instr === null) return;
+ $actualType = match ($instr::class) {
+ Instrs\Numeric\F32Const::class => ValType::F32,
+ Instrs\Numeric\F64Const::class => ValType::F64,
+ Instrs\Numeric\I32Const::class => ValType::I32,
+ Instrs\Numeric\I64Const::class => ValType::I64,
+ Instrs\Reference\RefFunc::class => TODO,
+ Instrs\Reference\RefNull::class => TODO,
+ Instrs\Variable\GlobalGet::class => TODO,
+ default => $this->addError('expr is not constant'),
+ };
+ }
+
+ private function validateElems(Context $c): void
+ {
+ foreach ($this->module->elems as $i => $elem) {
+ $this->validateConstantExpr($elem->init, $elem->type, $c);
+ todo();
+ }
+ }
+
+ private function validateDatas(Context $c): void
+ {
+ foreach ($this->module->datas as $i => $data) {
+ todo();
+ }
+ }
+
+ private function validateStart(): void
+ {
+ TODO();
+ }
+
+ private function validateImports(): void
+ {
+ TODO();
+ }
+
+ private function validateExports(): void
+ {
+ TODO();
+ }
+
+ private function validateMemsCount(): void
+ {
+ TODO();
+ }
+
+ private function validateExportsHaveDifferentNames(): void
+ {
+ TODO();
+ }
+
+ /**
+ * @param Func $func
+ */
+ private function validateFunc($func): void
+ {
+ // Reset validation state for each function
+ $this->valueStack = [];
+ $this->controlStack = [];
+
+ // Extract actual function signature types from func
+ if (! isset($this->module->types[$func->type])) {
+ $this->addError("Invalid function type index: {$func->type}");
+ return;
+ }
+
+ $funcType = $this->module->types[$func->type];
+ $this->pushCtrl('block', $funcType->params, $funcType->results);
+
+ $this->validateInstrs($func->body);
+ $this->validateEnd();
+ }
+
+ /**
+ * @param list<Instr> $instrs
+ */
+ private function validateInstrs(array $instrs): void
+ {
+ foreach ($instrs as $instr) {
+ $this->validateInstr($instr);
+ }
+ }
+
+ private function validateInstr(Instr $instr): void
+ {
+ switch ($instr::class) {
+ case Instrs\Numeric\I32Const::class:
+ $this->pushVal(ValType::I32);
+ break;
+
+ case Instrs\Numeric\I64Const::class:
+ $this->pushVal(ValType::I64);
+ break;
+
+ case Instrs\Numeric\F32Const::class:
+ $this->pushVal(ValType::F32);
+ break;
+
+ case Instrs\Numeric\F64Const::class:
+ $this->pushVal(ValType::F64);
+ break;
+
+ // Numeric instructions - F32 binary ops
+ case Instrs\Numeric\F32Add::class:
+ case Instrs\Numeric\F32Sub::class:
+ case Instrs\Numeric\F32Mul::class:
+ case Instrs\Numeric\F32Div::class:
+ case Instrs\Numeric\F32Min::class:
+ case Instrs\Numeric\F32Max::class:
+ case Instrs\Numeric\F32CopySign::class:
+ $this->validateF32BinaryOp();
+ break;
+
+ // Numeric instructions - F32 unary ops
+ case Instrs\Numeric\F32Abs::class:
+ case Instrs\Numeric\F32Neg::class:
+ case Instrs\Numeric\F32Ceil::class:
+ case Instrs\Numeric\F32Floor::class:
+ case Instrs\Numeric\F32Trunc::class:
+ case Instrs\Numeric\F32Nearest::class:
+ case Instrs\Numeric\F32Sqrt::class:
+ $this->validateF32UnaryOp();
+ break;
+
+ // Numeric instructions - F32 compare ops
+ case Instrs\Numeric\F32Eq::class:
+ case Instrs\Numeric\F32Ne::class:
+ case Instrs\Numeric\F32Lt::class:
+ case Instrs\Numeric\F32Gt::class:
+ case Instrs\Numeric\F32Le::class:
+ case Instrs\Numeric\F32Ge::class:
+ $this->validateF32CompareOp();
+ break;
+
+ case Instrs\Numeric\I32Add::class:
+ case Instrs\Numeric\I32Sub::class:
+ case Instrs\Numeric\I32Mul::class:
+ case Instrs\Numeric\I32DivS::class:
+ case Instrs\Numeric\I32DivU::class:
+ case Instrs\Numeric\I32RemS::class:
+ case Instrs\Numeric\I32RemU::class:
+ case Instrs\Numeric\I32And::class:
+ case Instrs\Numeric\I32Or::class:
+ case Instrs\Numeric\I32Xor::class:
+ case Instrs\Numeric\I32Shl::class:
+ case Instrs\Numeric\I32ShrS::class:
+ case Instrs\Numeric\I32ShrU::class:
+ case Instrs\Numeric\I32RotL::class:
+ case Instrs\Numeric\I32RotR::class:
+ $this->validateI32BinaryOp();
+ break;
+
+ case Instrs\Numeric\I32Eqz::class:
+ case Instrs\Numeric\I32Clz::class:
+ case Instrs\Numeric\I32Ctz::class:
+ case Instrs\Numeric\I32Popcnt::class:
+ $this->validateI32UnaryOp();
+ break;
+
+ case Instrs\Numeric\I32Eq::class:
+ case Instrs\Numeric\I32Ne::class:
+ case Instrs\Numeric\I32LtS::class:
+ case Instrs\Numeric\I32LtU::class:
+ case Instrs\Numeric\I32GtS::class:
+ case Instrs\Numeric\I32GtU::class:
+ case Instrs\Numeric\I32LeS::class:
+ case Instrs\Numeric\I32LeU::class:
+ case Instrs\Numeric\I32GeS::class:
+ case Instrs\Numeric\I32GeU::class:
+ $this->validateI32CompareOp();
+ break;
+
+ case Instrs\Parametric\Drop::class:
+ $this->popVal();
+ break;
+
+ case Instrs\Control\Nop::class:
+ break;
+
+ case Instrs\Parametric\Select::class:
+ $this->validateSelect();
+ break;
+
+ case Instrs\Control\Unreachable::class:
+ $this->unreachable();
+ break;
+
+ case Instrs\Control\Block::class:
+ $this->validateBlock($instr);
+ break;
+
+ case Instrs\Control\Loop::class:
+ $this->validateLoop($instr);
+ break;
+
+ case Instrs\Control\If_::class:
+ $this->validateIf($instr);
+ break;
+
+ case Instrs\Control\End::class:
+ $this->validateEnd();
+ break;
+
+ case Instrs\Control\Else_::class:
+ $this->validateElse();
+ break;
+
+ case Instrs\Control\Br::class:
+ $this->validateBr($instr);
+ break;
+
+ case Instrs\Control\BrIf::class:
+ $this->validateBrIf($instr);
+ break;
+
+ case Instrs\Control\BrTable::class:
+ $this->validateBrTable($instr);
+ break;
+
+ case Instrs\Control\Call::class:
+ $this->validateCall($instr);
+ break;
+
+ case Instrs\Control\CallIndirect::class:
+ $this->validateCallIndirect($instr);
+ break;
+
+ case Instrs\Control\Return_::class:
+ $this->validateReturn();
+ break;
+
+ // Numeric instructions - F64 binary ops
+ case Instrs\Numeric\F64Add::class:
+ case Instrs\Numeric\F64Sub::class:
+ case Instrs\Numeric\F64Mul::class:
+ case Instrs\Numeric\F64Div::class:
+ case Instrs\Numeric\F64Min::class:
+ case Instrs\Numeric\F64Max::class:
+ case Instrs\Numeric\F64CopySign::class:
+ $this->validateF64BinaryOp();
+ break;
+
+ // Numeric instructions - F64 unary ops
+ case Instrs\Numeric\F64Abs::class:
+ case Instrs\Numeric\F64Neg::class:
+ case Instrs\Numeric\F64Ceil::class:
+ case Instrs\Numeric\F64Floor::class:
+ case Instrs\Numeric\F64Trunc::class:
+ case Instrs\Numeric\F64Nearest::class:
+ case Instrs\Numeric\F64Sqrt::class:
+ $this->validateF64UnaryOp();
+ break;
+
+ // Numeric instructions - F64 compare ops
+ case Instrs\Numeric\F64Eq::class:
+ case Instrs\Numeric\F64Ne::class:
+ case Instrs\Numeric\F64Lt::class:
+ case Instrs\Numeric\F64Gt::class:
+ case Instrs\Numeric\F64Le::class:
+ case Instrs\Numeric\F64Ge::class:
+ $this->validateF64CompareOp();
+ break;
+
+ // Numeric instructions - I64 binary ops
+ case Instrs\Numeric\I64Add::class:
+ case Instrs\Numeric\I64Sub::class:
+ case Instrs\Numeric\I64Mul::class:
+ case Instrs\Numeric\I64DivS::class:
+ case Instrs\Numeric\I64DivU::class:
+ case Instrs\Numeric\I64RemS::class:
+ case Instrs\Numeric\I64RemU::class:
+ case Instrs\Numeric\I64And::class:
+ case Instrs\Numeric\I64Or::class:
+ case Instrs\Numeric\I64Xor::class:
+ case Instrs\Numeric\I64Shl::class:
+ case Instrs\Numeric\I64ShrS::class:
+ case Instrs\Numeric\I64ShrU::class:
+ case Instrs\Numeric\I64RotL::class:
+ case Instrs\Numeric\I64RotR::class:
+ $this->validateI64BinaryOp();
+ break;
+
+ // Numeric instructions - I64 unary ops
+ case Instrs\Numeric\I64Eqz::class:
+ case Instrs\Numeric\I64Clz::class:
+ case Instrs\Numeric\I64Ctz::class:
+ case Instrs\Numeric\I64Popcnt::class:
+ $this->validateI64UnaryOp();
+ break;
+
+ // Numeric instructions - I64 compare ops
+ case Instrs\Numeric\I64Eq::class:
+ case Instrs\Numeric\I64Ne::class:
+ case Instrs\Numeric\I64LtS::class:
+ case Instrs\Numeric\I64LtU::class:
+ case Instrs\Numeric\I64GtS::class:
+ case Instrs\Numeric\I64GtU::class:
+ case Instrs\Numeric\I64LeS::class:
+ case Instrs\Numeric\I64LeU::class:
+ case Instrs\Numeric\I64GeS::class:
+ case Instrs\Numeric\I64GeU::class:
+ $this->validateI64CompareOp();
+ break;
+
+ // Variable instructions
+ case Instrs\Variable\LocalGet::class:
+ $this->validateLocalGet($instr);
+ break;
+
+ case Instrs\Variable\LocalSet::class:
+ $this->validateLocalSet($instr);
+ break;
+
+ case Instrs\Variable\LocalTee::class:
+ $this->validateLocalTee($instr);
+ break;
+
+ case Instrs\Variable\GlobalGet::class:
+ $this->validateGlobalGet($instr);
+ break;
+
+ case Instrs\Variable\GlobalSet::class:
+ $this->validateGlobalSet($instr);
+ break;
+
+ // Reference instructions
+ case Instrs\Reference\RefNull::class:
+ $this->validateRefNull($instr);
+ break;
+
+ case Instrs\Reference\RefIsNull::class:
+ $this->validateRefIsNull();
+ break;
+
+ case Instrs\Reference\RefFunc::class:
+ $this->validateRefFunc($instr);
+ break;
+
+ // Type conversion instructions
+ case Instrs\Numeric\I32WrapI64::class:
+ $this->validateI32WrapI64();
+ break;
+
+ case Instrs\Numeric\I64ExtendI32S::class:
+ case Instrs\Numeric\I64ExtendI32U::class:
+ $this->validateI64ExtendI32();
+ break;
+
+ case Instrs\Numeric\F32DemoteF64::class:
+ $this->validateF32DemoteF64();
+ break;
+
+ case Instrs\Numeric\F64PromoteF32::class:
+ $this->validateF64PromoteF32();
+ break;
+
+ default:
+ $this->addError('Unsupported instruction: ' . $instr::class);
+ }
+ }
+
+ private function pushVal(?ValType $type): void
+ {
+ $this->valueStack[] = $type;
+ }
+
+ private function popVal(): ?ValType
+ {
+ $currentFrame = $this->controlStack[0] ?? throw new ValidationFailureException('No control frame available');
+ if (count($this->valueStack) === $currentFrame->height && $currentFrame->unreachable) {
+ return null;
+ }
+ if (count($this->valueStack) === $currentFrame->height) {
+ throw new ValidationFailureException('Value stack underflow');
+ }
+ return array_pop($this->valueStack);
+ }
+
+ private function popValExpected(?ValType $expect): ?ValType
+ {
+ $actual = $this->popVal();
+ if ($actual !== $expect && $actual !== null && $expect !== null) {
+ $this->addError('Type mismatch');
+ }
+ return $actual;
+ }
+
+ /**
+ * @param list<ValType> $types
+ */
+ private function pushVals(array $types): void
+ {
+ foreach ($types as $type) {
+ $this->pushVal($type);
+ }
+ }
+
+ /**
+ * @param list<ValType> $types
+ * @return list<ValType>
+ */
+ private function popVals(array $types): array
+ {
+ $popped = [];
+ foreach (array_reverse($types) as $type) {
+ $poppedVal = $this->popValExpected($type);
+ array_unshift($popped, $poppedVal ?? $type);
+ }
+ return $popped;
+ }
+
+ /**
+ * @param list<ValType> $in
+ * @param list<ValType> $out
+ */
+ private function pushCtrl(string $opcode, array $in, array $out): void
+ {
+ $frame = new ControlFrame($opcode, $in, $out, count($this->valueStack), false);
+ array_unshift($this->controlStack, $frame);
+ $this->pushVals($in);
+ }
+
+ private function popCtrl(): ControlFrame
+ {
+ if (count($this->controlStack) === 0) {
+ throw new ValidationFailureException('Control stack underflow');
+ }
+ $frame = $this->controlStack[0];
+ $this->popVals($frame->endTypes);
+ if (count($this->valueStack) !== $frame->height) {
+ throw new ValidationFailureException('Invalid stack height');
+ }
+ array_shift($this->controlStack);
+ return $frame;
+ }
+
+ /**
+ * @return list<ValType>
+ */
+ private function labelTypes(ControlFrame $frame): array
+ {
+ return $frame->opcode === 'loop' ? $frame->startTypes : $frame->endTypes;
+ }
+
+ private function unreachable(): void
+ {
+ $currentFrame = $this->controlStack[0] ?? throw new ValidationFailureException('No control frame available');
+ $this->valueStack = array_slice($this->valueStack, 0, $currentFrame->height);
+ // Replace current frame with unreachable version
+ $this->controlStack[0] = new ControlFrame(
+ $currentFrame->opcode,
+ $currentFrame->startTypes,
+ $currentFrame->endTypes,
+ $currentFrame->height,
+ true
+ );
+ }
+
+ private function assertTrue(bool $x, string $message): void
+ {
+ if (!$x) {$this->addError($message);}
+ }
+
+ private function addError(string $message): void
+ {
+ $this->errors[] = $message;
+ }
+
+ private function validateI32BinaryOp(): void
+ {
+ $this->popValExpected(ValType::I32);
+ $this->popValExpected(ValType::I32);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateI32UnaryOp(): void
+ {
+ $this->popValExpected(ValType::I32);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateI32CompareOp(): void
+ {
+ $this->popValExpected(ValType::I32);
+ $this->popValExpected(ValType::I32);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateSelect(): void
+ {
+ $this->popValExpected(ValType::I32);
+ $t1 = $this->popVal();
+ $t2 = $this->popVal();
+ if (! (($t1 === null || $t1->isNum()) && ($t2 === null || $t2->isNum()) || ($t1 === null || $t1->isVec()) && ($t2 === null || $t2->isVec()))) {
+ $this->addError('Invalid select operand types');
+ return;
+ }
+ if ($t1 !== $t2 && $t1 !== null && $t2 !== null) {
+ $this->addError('Type mismatch in select');
+ }
+ $this->pushVal($t1 === null ? $t2 : $t1);
+ }
+
+ private function validateBlock(Instrs\Control\Block $instr): void
+ {
+ [$paramTypes, $resultTypes] = $this->extractBlockTypes($instr->type);
+ $this->popVals($paramTypes);
+ $this->pushCtrl('block', $paramTypes, $resultTypes);
+ }
+
+ private function validateLoop(Instrs\Control\Loop $instr): void
+ {
+ [$paramTypes, $resultTypes] = $this->extractBlockTypes($instr->type);
+ $this->popVals($paramTypes);
+ $this->pushCtrl('loop', $paramTypes, $resultTypes);
+ }
+
+ private function validateIf(Instrs\Control\If_ $instr): void
+ {
+ [$paramTypes, $resultTypes] = $this->extractBlockTypes($instr->type);
+ $this->popValExpected(ValType::I32);
+ $this->popVals($paramTypes);
+ $this->pushCtrl('if', $paramTypes, $resultTypes);
+ }
+
+ private function validateEnd(): void
+ {
+ $frame = $this->popCtrl();
+ $this->pushVals($frame->endTypes);
+ }
+
+ private function validateElse(): void
+ {
+ $frame = $this->popCtrl();
+ if ($frame->opcode !== 'if') {
+ $this->addError('else without matching if');
+ return;
+ }
+ $this->pushCtrl('else', $frame->startTypes, $frame->endTypes);
+ }
+
+ private function validateBr(Instrs\Control\Br $instr): void
+ {
+ $labelIdx = $instr->label;
+ if (count($this->controlStack) <= $labelIdx) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ $targetFrame = $this->controlStack[$labelIdx] ?? null;
+ if ($targetFrame === null) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ $this->popVals($this->labelTypes($targetFrame));
+ $this->unreachable();
+ }
+
+ private function validateBrIf(Instrs\Control\BrIf $instr): void
+ {
+ $labelIdx = $instr->label;
+ if (count($this->controlStack) <= $labelIdx) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ $this->popValExpected(ValType::I32);
+ $targetFrame = $this->controlStack[$labelIdx] ?? null;
+ if ($targetFrame === null) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ $labelType = $this->labelTypes($targetFrame);
+ $this->popVals($labelType);
+ $this->pushVals($labelType);
+ }
+
+ private function validateBrTable(Instrs\Control\BrTable $instr): void
+ {
+ $labelIndices = $instr->labelTable;
+ $defaultLabel = $instr->defaultLabel;
+ $this->popValExpected(ValType::I32);
+
+ if (count($this->controlStack) <= $defaultLabel) {
+ $this->addError('Invalid default label index');
+ return;
+ }
+
+ $defaultFrame = $this->controlStack[$defaultLabel] ?? null;
+ if ($defaultFrame === null) {
+ $this->addError('Invalid default label');
+ return;
+ }
+ $arity = count($this->labelTypes($defaultFrame));
+
+ foreach ($labelIndices as $labelIdx) {
+ if (count($this->controlStack) <= $labelIdx) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ $labelFrame = $this->controlStack[$labelIdx] ?? null;
+ if ($labelFrame === null) {
+ $this->addError('Invalid label index');
+ return;
+ }
+ if (count($this->labelTypes($labelFrame)) !== $arity) {
+ $this->addError('Inconsistent label arities');
+ return;
+ }
+ $types = $this->labelTypes($labelFrame);
+ $this->pushVals($this->popVals($types));
+ }
+
+ $this->popVals($this->labelTypes($defaultFrame));
+ $this->unreachable();
+ }
+
+ private function validateF32BinaryOp(): void
+ {
+ $this->popValExpected(ValType::F32);
+ $this->popValExpected(ValType::F32);
+ $this->pushVal(ValType::F32);
+ }
+
+ private function validateF32UnaryOp(): void
+ {
+ $this->popValExpected(ValType::F32);
+ $this->pushVal(ValType::F32);
+ }
+
+ private function validateF32CompareOp(): void
+ {
+ $this->popValExpected(ValType::F32);
+ $this->popValExpected(ValType::F32);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateF64BinaryOp(): void
+ {
+ $this->popValExpected(ValType::F64);
+ $this->popValExpected(ValType::F64);
+ $this->pushVal(ValType::F64);
+ }
+
+ private function validateF64UnaryOp(): void
+ {
+ $this->popValExpected(ValType::F64);
+ $this->pushVal(ValType::F64);
+ }
+
+ private function validateF64CompareOp(): void
+ {
+ $this->popValExpected(ValType::F64);
+ $this->popValExpected(ValType::F64);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateI64BinaryOp(): void
+ {
+ $this->popValExpected(ValType::I64);
+ $this->popValExpected(ValType::I64);
+ $this->pushVal(ValType::I64);
+ }
+
+ private function validateI64UnaryOp(): void
+ {
+ $this->popValExpected(ValType::I64);
+ $this->pushVal(ValType::I64);
+ }
+
+ private function validateI64CompareOp(): void
+ {
+ $this->popValExpected(ValType::I64);
+ $this->popValExpected(ValType::I64);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateCall(Instrs\Control\Call $instr): void
+ {
+ // TODO: Validate function signature from module context
+ $this->addError('Call instruction validation not fully implemented');
+ }
+
+ private function validateCallIndirect(Instrs\Control\CallIndirect $instr): void
+ {
+ // TODO: Validate function table and type signature
+ $this->popValExpected(ValType::I32);
+ $this->addError('CallIndirect instruction validation not fully implemented');
+ }
+
+ private function validateReturn(): void
+ {
+ if (count($this->controlStack) === 0) {
+ $this->addError('Return outside function');
+ return;
+ }
+ $funcFrame = $this->controlStack[count($this->controlStack) - 1];
+ $this->popVals($funcFrame->endTypes);
+ $this->unreachable();
+ }
+
+ private function validateLocalGet(Instrs\Variable\LocalGet $instr): void
+ {
+ // TODO: Validate local variable index and type from function context
+ $this->pushVal(null);
+ $this->addError('LocalGet instruction validation not fully implemented');
+ }
+
+ private function validateLocalSet(Instrs\Variable\LocalSet $instr): void
+ {
+ // TODO: Validate local variable index and type from function context
+ $this->popVal();
+ $this->addError('LocalSet instruction validation not fully implemented');
+ }
+
+ private function validateLocalTee(Instrs\Variable\LocalTee $instr): void
+ {
+ // TODO: Validate local variable index and type from function context
+ $val = $this->popVal();
+ $this->pushVal($val);
+ $this->addError('LocalTee instruction validation not fully implemented');
+ }
+
+ private function validateGlobalGet(Instrs\Variable\GlobalGet $instr): void
+ {
+ // TODO: Validate global variable index and type from module context
+ $this->pushVal(null);
+ $this->addError('GlobalGet instruction validation not fully implemented');
+ }
+
+ private function validateGlobalSet(Instrs\Variable\GlobalSet $instr): void
+ {
+ // TODO: Validate global variable index and type from module context
+ $this->popVal();
+ $this->addError('GlobalSet instruction validation not fully implemented');
+ }
+
+ private function validateRefNull(Instrs\Reference\RefNull $instr): void
+ {
+ // TODO: Push the correct reference type based on $instr->type
+ $this->pushVal(null);
+ $this->addError('RefNull instruction validation not fully implemented');
+ }
+
+ private function validateRefIsNull(): void
+ {
+ $this->popVal(); // Pop reference value
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateRefFunc(Instrs\Reference\RefFunc $instr): void
+ {
+ // TODO: Validate function index from module context
+ $this->pushVal(null);
+ $this->addError('RefFunc instruction validation not fully implemented');
+ }
+
+ private function validateI32WrapI64(): void
+ {
+ $this->popValExpected(ValType::I64);
+ $this->pushVal(ValType::I32);
+ }
+
+ private function validateI64ExtendI32(): void
+ {
+ $this->popValExpected(ValType::I32);
+ $this->pushVal(ValType::I64);
+ }
+
+ private function validateF32DemoteF64(): void
+ {
+ $this->popValExpected(ValType::F64);
+ $this->pushVal(ValType::F32);
+ }
+
+ private function validateF64PromoteF32(): void
+ {
+ $this->popValExpected(ValType::F32);
+ $this->pushVal(ValType::F64);
+ }
+
+ /**
+ * @return array{list<ValType>, list<ValType>}
+ */
+ private function extractBlockTypes(BlockType $blockType): array
+ {
+ $funcType = $this->expandBlockType($blockType);
+ return [$funcType->params, $funcType->results];
+ }
+
+ private function expandBlockType(BlockType $blockType): FuncType
+ {
+ if ($blockType instanceof BlockTypes\TypeIdx) {
+ if (! isset($this->module->types[$blockType->inner])) {
+ $this->addError("Invalid type index: {$blockType->inner}");
+ return new FuncType([], []);
+ }
+ return $this->module->types[$blockType->inner];
+ } elseif ($blockType instanceof BlockTypes\ValType) {
+ $t = $blockType->inner;
+ return new FuncType(
+ [],
+ $t === null ? [] : [$t],
+ );
+ }
+ $this->addError('Unknown block type: ' . $blockType::class);
+ return new FuncType([], []);
+ }
+}