diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 23:22:25 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 23:22:35 +0900 |
| commit | 39e6c4bfb1f3fb96bba47e3eec8e6451038a3d22 (patch) | |
| tree | 619472dd31a38ae277299495d0ff213e31093dfb | |
| parent | 2bea3784061a33870d4678af011ffc6fc73c6ad3 (diff) | |
| download | php-waddiwasi-39e6c4bfb1f3fb96bba47e3eec8e6451038a3d22.tar.gz php-waddiwasi-39e6c4bfb1f3fb96bba47e3eec8e6451038a3d22.tar.zst php-waddiwasi-39e6c4bfb1f3fb96bba47e3eec8e6451038a3d22.zip | |
| -rw-r--r-- | TODO | 1 | ||||
| -rw-r--r-- | src/WebAssembly/BinaryFormat/Decoder.php | 3 | ||||
| -rw-r--r-- | src/WebAssembly/Structure/Types/ValType.php | 24 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/Context.php | 42 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/ControlFrame.php | 24 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/FunctionValidator.php | 1015 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/ValidationFailureException.php | 11 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/ValidationResult.php | 16 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/ValidationStack.php | 154 | ||||
| -rw-r--r-- | src/WebAssembly/Validation/Validator.php | 426 | ||||
| -rw-r--r-- | tests/src/SpecTestsuites/SpecTestsuiteBase.php | 15 |
11 files changed, 1726 insertions, 5 deletions
@@ -1,5 +1,4 @@ * Support text format (.wat) -* Implement validation * Provide sane bindings to PHP * Write PHPDoc for public APIs * Provide high-level APIs diff --git a/src/WebAssembly/BinaryFormat/Decoder.php b/src/WebAssembly/BinaryFormat/Decoder.php index 90b1706..87e5640 100644 --- a/src/WebAssembly/BinaryFormat/Decoder.php +++ b/src/WebAssembly/BinaryFormat/Decoder.php @@ -38,7 +38,6 @@ use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; use function array_reduce; use function assert; use function count; -use function get_class; use function in_array; use function is_int; use function ord; @@ -980,7 +979,7 @@ final class Decoder $result = []; while (true) { $instr = $this->decodeInstr(); - if (in_array(get_class($instr), $delimiters, true)) { + if (in_array($instr::class, $delimiters, true)) { return [$result, $instr]; } $result[] = $instr; diff --git a/src/WebAssembly/Structure/Types/ValType.php b/src/WebAssembly/Structure/Types/ValType.php index ace4604..02e1113 100644 --- a/src/WebAssembly/Structure/Types/ValType.php +++ b/src/WebAssembly/Structure/Types/ValType.php @@ -13,4 +13,28 @@ enum ValType case V128; case FuncRef; case ExternRef; + + public function isNum(): bool + { + return match ($this) { + ValType::I32 , ValType::I64 , ValType::F32 , ValType::F64 => true, + default => false, + }; + } + + public function isVec(): bool + { + return match ($this) { + ValType::V128 => true, + default => false, + }; + } + + public function isRef(): bool + { + return match ($this) { + ValType::FuncRef , ValType::ExternRef => true, + default => false, + }; + } } diff --git a/src/WebAssembly/Validation/Context.php b/src/WebAssembly/Validation/Context.php new file mode 100644 index 0000000..e63fb0d --- /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 readonly 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<ValType> $locals + * @param list<list<ValType>> $labels + * @param list<ValType> $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..e956b4c --- /dev/null +++ b/src/WebAssembly/Validation/ControlFrame.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\WebAssembly\Validation; + +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; + +final readonly class ControlFrame +{ + /** + * @param class-string $opcode + * @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/FunctionValidator.php b/src/WebAssembly/Validation/FunctionValidator.php new file mode 100644 index 0000000..2b94f02 --- /dev/null +++ b/src/WebAssembly/Validation/FunctionValidator.php @@ -0,0 +1,1015 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\WebAssembly\Validation; + +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\Types\FuncType; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\Mut; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; +use function count; +use function in_array; + +final class FunctionValidator +{ + /** + * @var list<string> + */ + private array $errors = []; + + public function __construct( + private readonly Context $context, + private readonly ValidationStack $validationStack, + ) { + } + + /** + * @param list<Instr> $body + * @return list<string> + */ + public function validate(array $body): array + { + $this->validateInstrs($body); + $this->validateEnd(); + + return $this->errors; + } + + /** + * @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->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I64Const::class: + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\F32Const::class: + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F64Const::class: + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Numeric\I32Eqz::class: + case Instrs\Numeric\I32Clz::class: + case Instrs\Numeric\I32Ctz::class: + case Instrs\Numeric\I32Popcnt::class: + case Instrs\Numeric\I32Extend8S::class: + case Instrs\Numeric\I32Extend16S::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::I32); + 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->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::I32); + 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->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I64Eqz::class: + case Instrs\Numeric\I64Clz::class: + case Instrs\Numeric\I64Ctz::class: + case Instrs\Numeric\I64Popcnt::class: + case Instrs\Numeric\I64Extend8S::class: + case Instrs\Numeric\I64Extend16S::class: + case Instrs\Numeric\I64Extend32S::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::I64); + break; + + 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->validationStack->popValOf(ValType::I64); + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::I64); + break; + + 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->validationStack->popValOf(ValType::I64); + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::I32); + break; + + 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->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::F32); + break; + + 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->validationStack->popValOf(ValType::F32); + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::F32); + break; + + 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->validationStack->popValOf(ValType::F32); + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::I32); + break; + + 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->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::F64); + break; + + 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->validationStack->popValOf(ValType::F64); + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::F64); + break; + + 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->validationStack->popValOf(ValType::F64); + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I32WrapI64::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I32TruncF32S::class: + case Instrs\Numeric\I32TruncF32U::class: + case Instrs\Numeric\I32TruncSatF32S::class: + case Instrs\Numeric\I32TruncSatF32U::class: + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I32TruncF64S::class: + case Instrs\Numeric\I32TruncF64U::class: + case Instrs\Numeric\I32TruncSatF64S::class: + case Instrs\Numeric\I32TruncSatF64U::class: + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I64ExtendI32S::class: + case Instrs\Numeric\I64ExtendI32U::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\I64TruncF32S::class: + case Instrs\Numeric\I64TruncF32U::class: + case Instrs\Numeric\I64TruncSatF32S::class: + case Instrs\Numeric\I64TruncSatF32U::class: + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\I64TruncF64S::class: + case Instrs\Numeric\I64TruncF64U::class: + case Instrs\Numeric\I64TruncSatF64S::class: + case Instrs\Numeric\I64TruncSatF64U::class: + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\F32ConvertI32S::class: + case Instrs\Numeric\F32ConvertI32U::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F32ConvertI64S::class: + case Instrs\Numeric\F32ConvertI64U::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F32DemoteF64::class: + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F64ConvertI32S::class: + case Instrs\Numeric\F64ConvertI32U::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Numeric\F64ConvertI64S::class: + case Instrs\Numeric\F64ConvertI64U::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Numeric\F64PromoteF32::class: + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Numeric\I32ReinterpretF32::class: + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I64ReinterpretF64::class: + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\F32ReinterpretI32::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F64ReinterpretI64::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Numeric\I32ReinterpretF64::class: + $this->validationStack->popValOf(ValType::F64); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Numeric\I64ReinterpretF32::class: + $this->validationStack->popValOf(ValType::F32); + $this->validationStack->pushVal(ValType::I64); + break; + + case Instrs\Numeric\F32ReinterpretI64::class: + $this->validationStack->popValOf(ValType::I64); + $this->validationStack->pushVal(ValType::F32); + break; + + case Instrs\Numeric\F64ReinterpretI32::class: + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::F64); + break; + + case Instrs\Parametric\Drop::class: + $this->validationStack->popVal(); + break; + + case Instrs\Parametric\Select::class: + $this->validateSelect(); + break; + + case Instrs\Control\Nop::class: + break; + + case Instrs\Control\Unreachable::class: + $this->validationStack->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($instr); + 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; + + 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; + + 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; + + case Instrs\Table\TableGet::class: + $this->validateTableGet($instr); + break; + + case Instrs\Table\TableSet::class: + $this->validateTableSet($instr); + break; + + case Instrs\Table\TableSize::class: + $this->validateTableSize($instr); + break; + + case Instrs\Table\TableGrow::class: + $this->validateTableGrow($instr); + break; + + case Instrs\Table\TableFill::class: + $this->validateTableFill($instr); + break; + + case Instrs\Table\TableCopy::class: + $this->validateTableCopy($instr); + break; + + case Instrs\Table\TableInit::class: + $this->validateTableInit($instr); + break; + + case Instrs\Table\ElemDrop::class: + $this->validateElemDrop($instr); + break; + + case Instrs\Memory\I32Load::class: + $this->validateMemLoad($instr->align, 4, ValType::I32); + break; + + case Instrs\Memory\I32Load8S::class: + case Instrs\Memory\I32Load8U::class: + $this->validateMemLoad($instr->align, 1, ValType::I32); + break; + + case Instrs\Memory\I32Load16S::class: + case Instrs\Memory\I32Load16U::class: + $this->validateMemLoad($instr->align, 2, ValType::I32); + break; + + case Instrs\Memory\I64Load::class: + $this->validateMemLoad($instr->align, 8, ValType::I64); + break; + + case Instrs\Memory\I64Load8S::class: + case Instrs\Memory\I64Load8U::class: + $this->validateMemLoad($instr->align, 1, ValType::I64); + break; + + case Instrs\Memory\I64Load16S::class: + case Instrs\Memory\I64Load16U::class: + $this->validateMemLoad($instr->align, 2, ValType::I64); + break; + + case Instrs\Memory\I64Load32S::class: + case Instrs\Memory\I64Load32U::class: + $this->validateMemLoad($instr->align, 4, ValType::I64); + break; + + case Instrs\Memory\F32Load::class: + $this->validateMemLoad($instr->align, 4, ValType::F32); + break; + + case Instrs\Memory\F64Load::class: + $this->validateMemLoad($instr->align, 8, ValType::F64); + break; + + case Instrs\Memory\I32Store::class: + $this->validateMemStore($instr->align, 4, ValType::I32); + break; + + case Instrs\Memory\I32Store8::class: + $this->validateMemStore($instr->align, 1, ValType::I32); + break; + + case Instrs\Memory\I32Store16::class: + $this->validateMemStore($instr->align, 2, ValType::I32); + break; + + case Instrs\Memory\I64Store::class: + $this->validateMemStore($instr->align, 8, ValType::I64); + break; + + case Instrs\Memory\I64Store8::class: + $this->validateMemStore($instr->align, 1, ValType::I64); + break; + + case Instrs\Memory\I64Store16::class: + $this->validateMemStore($instr->align, 2, ValType::I64); + break; + + case Instrs\Memory\I64Store32::class: + $this->validateMemStore($instr->align, 4, ValType::I64); + break; + + case Instrs\Memory\F32Store::class: + $this->validateMemStore($instr->align, 4, ValType::F32); + break; + + case Instrs\Memory\F64Store::class: + $this->validateMemStore($instr->align, 8, ValType::F64); + break; + + case Instrs\Memory\MemorySize::class: + $this->validateMemoryExists(); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Memory\MemoryGrow::class: + $this->validateMemoryExists(); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal(ValType::I32); + break; + + case Instrs\Memory\MemoryFill::class: + $this->validateMemoryExists(); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + break; + + case Instrs\Memory\MemoryCopy::class: + $this->validateMemoryExists(); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + break; + + case Instrs\Memory\MemoryInit::class: + $this->validateMemoryInit($instr); + break; + + case Instrs\Memory\DataDrop::class: + $this->validateDataDrop($instr); + break; + + default: + $this->addError('Unsupported instruction: ' . $instr::class); + } + } + + private function validateSelect(): void + { + $this->validationStack->popValOf(ValType::I32); + $t1 = $this->validationStack->popVal(); + $t2 = $this->validationStack->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->validationStack->pushVal($t1 ?? $t2); + } + + private function validateBlock(Instrs\Control\Block $instr): void + { + [$t1, $t2] = $this->extractBlockTypes($instr->type); + $this->validationStack->popVals($t1); + $this->validationStack->pushCtrl($instr::class, $t1, $t2); + $this->validateInstrs($instr->body); + $ctrl = $this->validationStack->popCtrl(); + $this->validationStack->pushVals($ctrl->endTypes); + } + + private function validateLoop(Instrs\Control\Loop $instr): void + { + [$t1, $t2] = $this->extractBlockTypes($instr->type); + $this->validationStack->popVals($t1); + $this->validationStack->pushCtrl($instr::class, $t1, $t2); + $this->validateInstrs($instr->body); + $ctrl = $this->validationStack->popCtrl(); + $this->validationStack->pushVals($ctrl->endTypes); + } + + private function validateIf(Instrs\Control\If_ $instr): void + { + [$t1, $t2] = $this->extractBlockTypes($instr->type); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popVals($t1); + $this->validationStack->pushCtrl($instr::class, $t1, $t2); + $this->validateInstrs($instr->thenBody); + $ctrl = $this->validationStack->popCtrl(); + $this->validationStack->pushCtrl(Instrs\Control\Else_::class, $ctrl->startTypes, $ctrl->endTypes); + $this->validateInstrs($instr->elseBody); + $ctrl = $this->validationStack->popCtrl(); + $this->validationStack->pushVals($ctrl->endTypes); + } + + private function validateEnd(): void + { + $ctrl = $this->validationStack->popCtrl(); + $this->validationStack->pushVals($ctrl->endTypes); + } + + private function validateElse(Instrs\Control\Else_ $instr): void + { + $ctrl = $this->validationStack->popCtrl(); + if ($ctrl->opcode !== Instrs\Control\If_::class) { + $this->addError('else without matching if'); + return; + } + $this->validationStack->pushCtrl($instr::class, $ctrl->startTypes, $ctrl->endTypes); + } + + private function validateBr(Instrs\Control\Br $instr): void + { + $ctrl = $this->validationStack->getControl($instr->label); + if ($ctrl === null) { + $this->addError('Invalid label index'); + return; + } + $this->validationStack->popVals($this->validationStack->labelTypes($ctrl)); + $this->validationStack->unreachable(); + } + + private function validateBrIf(Instrs\Control\BrIf $instr): void + { + $ctrl = $this->validationStack->getControl($instr->label); + if ($ctrl === null) { + $this->addError('Invalid label index'); + return; + } + $this->validationStack->popValOf(ValType::I32); + $labelType = $this->validationStack->labelTypes($ctrl); + $this->validationStack->popVals($labelType); + $this->validationStack->pushVals($labelType); + } + + private function validateBrTable(Instrs\Control\BrTable $instr): void + { + $this->validationStack->popValOf(ValType::I32); + + $defaultCtrl = $this->validationStack->getControl($instr->defaultLabel); + if ($defaultCtrl === null) { + $this->addError('Invalid default label'); + return; + } + $arity = count($this->validationStack->labelTypes($defaultCtrl)); + + foreach ($instr->labelTable as $labelIdx) { + $labelCtrl = $this->validationStack->getControl($labelIdx); + if ($labelCtrl === null) { + $this->addError('Invalid label index'); + return; + } + if (count($this->validationStack->labelTypes($labelCtrl)) !== $arity) { + $this->addError('Inconsistent label arity'); + return; + } + $types = $this->validationStack->labelTypes($labelCtrl); + $this->validationStack->pushVals($this->validationStack->popVals($types)); + } + + $this->validationStack->popVals($this->validationStack->labelTypes($defaultCtrl)); + $this->validationStack->unreachable(); + } + + private function validateCall(Instrs\Control\Call $instr): void + { + $funcIdx = $instr->func; + if (! isset($this->context->funcs[$funcIdx])) { + $this->addError("Invalid function index: {$funcIdx}"); + return; + } + $funcType = $this->context->funcs[$funcIdx]; + $this->validationStack->popVals($funcType->params); + $this->validationStack->pushVals($funcType->results); + } + + private function validateCallIndirect(Instrs\Control\CallIndirect $instr): void + { + $tableIdx = $instr->funcTable; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $tableType = $this->context->tables[$tableIdx]; + if ($tableType->refType !== ValType::FuncRef) { + $this->addError('call_indirect requires a table with funcref element type'); + } + $typeIdx = $instr->type; + if (! isset($this->context->types[$typeIdx])) { + $this->addError("Invalid type index: {$typeIdx}"); + return; + } + $funcType = $this->context->types[$typeIdx]; + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popVals($funcType->params); + $this->validationStack->pushVals($funcType->results); + } + + private function validateReturn(): void + { + $this->validationStack->popVals($this->context->return); + $this->validationStack->unreachable(); + } + + private function validateLocalGet(Instrs\Variable\LocalGet $instr): void + { + $localIdx = $instr->var; + if (! isset($this->context->locals[$localIdx])) { + $this->addError("Invalid local index: {$localIdx}"); + return; + } + $localType = $this->context->locals[$localIdx]; + $this->validationStack->pushVal($localType); + } + + private function validateLocalSet(Instrs\Variable\LocalSet $instr): void + { + $localIdx = $instr->var; + if (! isset($this->context->locals[$localIdx])) { + $this->addError("Invalid local index: {$localIdx}"); + return; + } + $localType = $this->context->locals[$localIdx]; + $this->validationStack->popValOf($localType); + } + + private function validateLocalTee(Instrs\Variable\LocalTee $instr): void + { + $localIdx = $instr->var; + if (! isset($this->context->locals[$localIdx])) { + $this->addError("Invalid local index: {$localIdx}"); + return; + } + $type = $this->context->locals[$localIdx]; + $this->validationStack->popValOf($type); + $this->validationStack->pushVal($type); + } + + private function validateGlobalGet(Instrs\Variable\GlobalGet $instr): void + { + $globalIdx = $instr->var; + if (! isset($this->context->globals[$globalIdx])) { + $this->addError("Invalid global index: {$globalIdx}"); + return; + } + $this->validationStack->pushVal($this->context->globals[$globalIdx]->valType); + } + + private function validateGlobalSet(Instrs\Variable\GlobalSet $instr): void + { + $globalIdx = $instr->var; + if (! isset($this->context->globals[$globalIdx])) { + $this->addError("Invalid global index: {$globalIdx}"); + return; + } + $globalType = $this->context->globals[$globalIdx]; + if ($globalType->mut !== Mut::Var) { + $this->addError('Cannot set immutable global'); + } + $this->validationStack->popValOf($globalType->valType); + } + + private function validateRefNull(Instrs\Reference\RefNull $instr): void + { + $this->validationStack->pushVal($instr->type); + } + + private function validateRefIsNull(): void + { + $val = $this->validationStack->popVal(); + if ($val !== null && ! $val->isRef()) { + $this->addError('ref.is_null expects a reference type'); + } + $this->validationStack->pushVal(ValType::I32); + } + + private function validateRefFunc(Instrs\Reference\RefFunc $instr): void + { + $funcIdx = $instr->func; + if (! isset($this->context->funcs[$funcIdx])) { + $this->addError("unknown function {$funcIdx}"); + return; + } + if (! in_array($funcIdx, $this->context->refs, true)) { + $this->addError('undeclared function reference'); + return; + } + $this->validationStack->pushVal(ValType::FuncRef); + } + + private function validateTableGet(Instrs\Table\TableGet $instr): void + { + $tableIdx = $instr->table; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal($this->context->tables[$tableIdx]->refType); + } + + private function validateTableSet(Instrs\Table\TableSet $instr): void + { + $tableIdx = $instr->table; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $this->validationStack->popValOf($this->context->tables[$tableIdx]->refType); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateTableSize(Instrs\Table\TableSize $instr): void + { + $tableIdx = $instr->table; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $this->validationStack->pushVal(ValType::I32); + } + + private function validateTableGrow(Instrs\Table\TableGrow $instr): void + { + $tableIdx = $instr->table; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf($this->context->tables[$tableIdx]->refType); + $this->validationStack->pushVal(ValType::I32); + } + + private function validateTableFill(Instrs\Table\TableFill $instr): void + { + $tableIdx = $instr->table; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf($this->context->tables[$tableIdx]->refType); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateTableCopy(Instrs\Table\TableCopy $instr): void + { + $toIdx = $instr->to; + $fromIdx = $instr->from; + if (! isset($this->context->tables[$toIdx])) { + $this->addError("Invalid destination table index: {$toIdx}"); + return; + } + if (! isset($this->context->tables[$fromIdx])) { + $this->addError("Invalid source table index: {$fromIdx}"); + return; + } + if ($this->context->tables[$toIdx]->refType !== $this->context->tables[$fromIdx]->refType) { + $this->addError('table.copy requires tables with the same reference type'); + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateTableInit(Instrs\Table\TableInit $instr): void + { + $tableIdx = $instr->to; + $elemIdx = $instr->from; + if (! isset($this->context->tables[$tableIdx])) { + $this->addError("Invalid table index: {$tableIdx}"); + return; + } + if (! isset($this->context->elems[$elemIdx])) { + $this->addError("Invalid element segment index: {$elemIdx}"); + return; + } + if ($this->context->tables[$tableIdx]->refType !== $this->context->elems[$elemIdx]) { + $this->addError('table.init requires matching reference types'); + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateElemDrop(Instrs\Table\ElemDrop $instr): void + { + $elemIdx = $instr->elem; + if (! isset($this->context->elems[$elemIdx])) { + $this->addError("Invalid element segment index: {$elemIdx}"); + } + } + + private function validateMemoryExists(): void + { + if (! isset($this->context->mems[0])) { + $this->addError('Memory 0 is not defined'); + } + } + + private function validateMemLoad(int $align, int $maxAlign, ValType $resultType): void + { + $this->validateMemoryExists(); + if ((1 << $align) > $maxAlign) { + $this->addError("alignment must not be larger than natural alignment ({$maxAlign})"); + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->pushVal($resultType); + } + + private function validateMemStore(int $align, int $maxAlign, ValType $valueType): void + { + $this->validateMemoryExists(); + if ((1 << $align) > $maxAlign) { + $this->addError("alignment must not be larger than natural alignment ({$maxAlign})"); + } + $this->validationStack->popValOf($valueType); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateMemoryInit(Instrs\Memory\MemoryInit $instr): void + { + if (! isset($this->context->mems[0])) { + $this->addError('Memory 0 is not defined'); + } + $dataIdx = $instr->data; + if (! isset($this->context->datas[$dataIdx])) { + $this->addError("Invalid data segment index: {$dataIdx}"); + } + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + $this->validationStack->popValOf(ValType::I32); + } + + private function validateDataDrop(Instrs\Memory\DataDrop $instr): void + { + $dataIdx = $instr->data; + if (! isset($this->context->datas[$dataIdx])) { + $this->addError("Invalid data segment index: {$dataIdx}"); + } + } + + /** + * @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->context->types[$blockType->inner])) { + $this->addError("Invalid type index: {$blockType->inner}"); + return new FuncType([], []); + } + return $this->context->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([], []); + } + + private function addError(string $message): void + { + $this->errors[] = $message; + } +} 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/ValidationStack.php b/src/WebAssembly/Validation/ValidationStack.php new file mode 100644 index 0000000..53b47a7 --- /dev/null +++ b/src/WebAssembly/Validation/ValidationStack.php @@ -0,0 +1,154 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\WebAssembly\Validation; + +use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instrs; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; +use function array_pop; +use function array_reverse; +use function array_slice; +use function assert; +use function count; + +final class ValidationStack +{ + /** + * @var list<?ValType> + */ + private array $values = []; + + /** + * @var list<ControlFrame> + */ + private array $controls = []; + + public function __construct( + ) { + } + + public function pushVal(?ValType $type): void + { + $this->values[] = $type; + } + + /** + * @param list<?ValType> $types + */ + public function pushVals(array $types): void + { + foreach ($types as $type) { + $this->pushVal($type); + } + } + + public function popVal(): ?ValType + { + // Check stack underflow. + $currentCtrl = $this->getCurrentControl(); + if (count($this->values) === $currentCtrl->height) { + // If the current control frame is marked as unreachable, the stack behaves polymorphically. + if ($currentCtrl->unreachable) { + return null; + } + throw new ValidationFailureException('Value stack underflow'); + } + + return array_pop($this->values); + } + + public function popValOf(?ValType $expect): ?ValType + { + $actual = $this->popVal(); + if ($actual !== $expect && $actual !== null && $expect !== null) { + throw new ValidationFailureException('Type mismatch'); + } + return $actual; + } + + /** + * @param list<ValType> $types + * @return list<?ValType> + */ + public function popVals(array $types): array + { + $popped = []; + foreach (array_reverse($types) as $type) { + $popped[] = $this->popValOf($type); + } + return array_reverse($popped); + } + + /** + * @param class-string $opcode + * @param list<ValType> $in + * @param list<ValType> $out + */ + public function pushCtrl(string $opcode, array $in, array $out): void + { + $ctrl = new ControlFrame($opcode, $in, $out, count($this->values), false); + $this->controls[] = $ctrl; + $this->pushVals($in); + } + + public function popCtrl(): ControlFrame + { + if ($this->controlDepth() === 0) { + throw new ValidationFailureException('Control stack underflow'); + } + $ctrl = $this->getCurrentControl(); + $this->popVals($ctrl->endTypes); + if (count($this->values) !== $ctrl->height) { + throw new ValidationFailureException('Invalid stack height'); + } + array_pop($this->controls); + return $ctrl; + } + + /** + * @return list<ValType> + */ + public function labelTypes(ControlFrame $ctrl): array + { + return $ctrl->opcode === Instrs\Control\Loop::class ? $ctrl->startTypes : $ctrl->endTypes; + } + + public function unreachable(): void + { + $currentCtrl = $this->getCurrentControl(); + $this->values = array_slice($this->values, 0, $currentCtrl->height); + assert(count($this->controls) !== 0); + array_pop($this->controls); + $this->controls[] = new ControlFrame( + $currentCtrl->opcode, + $currentCtrl->startTypes, + $currentCtrl->endTypes, + $currentCtrl->height, + true, + ); + } + + public function controlDepth(): int + { + return count($this->controls); + } + + public function getControl(int $index): ?ControlFrame + { + $absIndex = count($this->controls) - 1 - $index; + if ($absIndex < 0) { + return null; + } + return $this->controls[$absIndex] ?? null; + } + + public function getCurrentControl(): ControlFrame + { + if ($this->controlDepth() === 0) { + throw new ValidationFailureException('No control frame available'); + } + assert(count($this->controls) !== 0); + return $this->controls[array_key_last($this->controls)]; + } +} diff --git a/src/WebAssembly/Validation/Validator.php b/src/WebAssembly/Validation/Validator.php new file mode 100644 index 0000000..60685e4 --- /dev/null +++ b/src/WebAssembly/Validation/Validator.php @@ -0,0 +1,426 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\WebAssembly\Validation; + +use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instr; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Instructions\Instrs; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\DataModes; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\ElemModes; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\ExportDescs; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\Func; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Modules\ImportDescs; +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\Mut; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\TableType; +use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; +use function array_fill; +use function array_map; +use function array_merge; +use function count; +use function in_array; + +final class Validator +{ + /** + * @var list<string> + */ + private array $errors = []; + + public function __construct( + private readonly Module $module, + ) { + } + + public function validate(): ValidationResult + { + try { + $this->validateModule(); + return new ValidationResult($this->errors); + } catch (ValidationFailureException $e) { + return new ValidationResult([...$this->errors, $e->getMessage()]); + } + } + + private function validateModule(): void + { + $importFuncTypes = []; + $importTableTypes = []; + $importMemTypes = []; + $importGlobalTypes = []; + foreach ($this->module->imports as $import) { + $desc = $import->desc; + if ($desc instanceof ImportDescs\Func) { + if (isset($this->module->types[$desc->func])) { + $importFuncTypes[] = $this->module->types[$desc->func]; + } else { + $this->addError("Invalid import function type index: {$desc->func}"); + $importFuncTypes[] = new FuncType([], []); + } + } elseif ($desc instanceof ImportDescs\Table) { + $importTableTypes[] = $desc->table; + } elseif ($desc instanceof ImportDescs\Mem) { + $importMemTypes[] = $desc->mem; + } elseif ($desc instanceof ImportDescs\Global_) { + $importGlobalTypes[] = $desc->global; + } + } + + $c = new Context( + types: $this->module->types, + funcs: array_merge( + $importFuncTypes, + array_map( + fn (Func $f) => $this->module->types[$f->type] ?? new FuncType([], []), + $this->module->funcs, + ), + ), + tables: array_merge( + $importTableTypes, + array_map(fn ($t) => $t->type, $this->module->tables), + ), + mems: array_merge( + $importMemTypes, + array_map(fn ($m) => $m->type, $this->module->mems), + ), + globals: array_merge( + $importGlobalTypes, + array_map(fn ($g) => $g->type, $this->module->globals), + ), + elems: array_map(fn ($e) => $e->type, $this->module->elems), + datas: array_fill(0, count($this->module->datas), true), + locals: [], + labels: [], + return: [], + refs: $this->collectRefs(), + ); + + $c_ = new Context( + types: $c->types, + funcs: $c->funcs, + tables: $c->tables, + mems: $c->mems, + globals: $importGlobalTypes, + elems: $c->elems, + datas: $c->datas, + locals: $c->locals, + labels: $c->labels, + return: $c->return, + refs: $c->refs, + ); + + $this->validateTypes(); + + $this->validateFuncs($c); + $this->validateTables($c); + $this->validateMems($c); + $this->validateGlobals($c_); + $this->validateElems($c_); + $this->validateDatas($c_); + + $this->validateStart($c); + $this->validateImports($c); + $this->validateExports($c); + + $this->validateMemsCount($c); + $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($c, $func); + } + } + + private function validateTables(Context $c): void + { + foreach ($this->module->tables as $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->addErrorUnless($n <= $k, 'invalid limits'); + if ($m !== null) { + $this->addErrorUnless($m <= $k, 'invalid limits'); + $this->addErrorUnless($n <= $m, 'invalid limits'); + } + } + + private function validateMems(Context $c): void + { + foreach ($this->module->mems as $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 $global) { + $this->validateConstantExpr($global->init, $global->type->valType, $c); + } + } + + /** + * @param list<Instr> $instrs + */ + private function validateConstantExpr(array $instrs, ValType $expectedType, Context $c): void + { + if (count($instrs) !== 1) { + $this->addError('constant expression required'); + return; + } + $instr = $instrs[0]; + $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 => $this->validateConstRefFunc($instr, $c), + Instrs\Reference\RefNull::class => $instr->type, + Instrs\Variable\GlobalGet::class => $this->validateConstGlobalGet($instr, $c), + default => null, + }; + if ($actualType === null) { + $this->addError('constant expression required'); + return; + } + if ($actualType !== $expectedType) { + $this->addError('type mismatch'); + } + } + + private function validateConstRefFunc(Instrs\Reference\RefFunc $instr, Context $c): ?ValType + { + if (! isset($c->funcs[$instr->func])) { + $this->addError("unknown function {$instr->func}"); + return null; + } + return ValType::FuncRef; + } + + private function validateConstGlobalGet(Instrs\Variable\GlobalGet $instr, Context $c): ?ValType + { + if (! isset($c->globals[$instr->var])) { + $this->addError("unknown global {$instr->var}"); + return null; + } + $global = $c->globals[$instr->var]; + if ($global->mut !== Mut::Const) { + $this->addError('constant expression required'); + return null; + } + return $global->valType; + } + + private function validateElems(Context $c): void + { + foreach ($this->module->elems as $elem) { + foreach ($elem->init as $initExpr) { + $this->validateConstantExpr($initExpr, $elem->type, $c); + } + if ($elem->mode instanceof ElemModes\Active) { + if (! isset($c->tables[$elem->mode->table])) { + $this->addError("unknown table {$elem->mode->table}"); + continue; + } + if ($c->tables[$elem->mode->table]->refType !== $elem->type) { + $this->addError('type mismatch'); + } + $this->validateConstantExpr($elem->mode->offset, ValType::I32, $c); + } + } + } + + private function validateDatas(Context $c): void + { + foreach ($this->module->datas as $data) { + if ($data->mode instanceof DataModes\Active) { + if (! isset($c->mems[$data->mode->memory])) { + $this->addError("unknown memory {$data->mode->memory}"); + continue; + } + $this->validateConstantExpr($data->mode->offset, ValType::I32, $c); + } + } + } + + private function validateStart(Context $c): void + { + $start = $this->module->start; + if ($start === null) { + return; + } + $funcIdx = $start->func; + if (! isset($c->funcs[$funcIdx])) { + $this->addError("Invalid start function index: {$funcIdx}"); + return; + } + $funcType = $c->funcs[$funcIdx]; + $this->addErrorUnless( + count($funcType->params) === 0 && count($funcType->results) === 0, + 'start function must have type [] -> []', + ); + } + + private function validateImports(Context $c): void + { + foreach ($this->module->imports as $import) { + $desc = $import->desc; + if ($desc instanceof ImportDescs\Func) { + $this->addErrorUnless( + isset($this->module->types[$desc->func]), + "Invalid import function type index: {$desc->func}", + ); + } elseif ($desc instanceof ImportDescs\Table) { + $this->validateTableType($desc->table); + } elseif ($desc instanceof ImportDescs\Mem) { + $this->validateMemType($desc->mem); + } elseif ($desc instanceof ImportDescs\Global_) { + // Global import types are always valid + } + } + } + + private function validateExports(Context $c): void + { + foreach ($this->module->exports as $export) { + $desc = $export->desc; + if ($desc instanceof ExportDescs\Func) { + $this->addErrorUnless( + isset($c->funcs[$desc->func]), + "Invalid export function index: {$desc->func}", + ); + } elseif ($desc instanceof ExportDescs\Table) { + $this->addErrorUnless( + isset($c->tables[$desc->table]), + "Invalid export table index: {$desc->table}", + ); + } elseif ($desc instanceof ExportDescs\Mem) { + $this->addErrorUnless( + isset($c->mems[$desc->mem]), + "Invalid export memory index: {$desc->mem}", + ); + } elseif ($desc instanceof ExportDescs\Global_) { + $this->addErrorUnless( + isset($c->globals[$desc->global]), + "Invalid export global index: {$desc->global}", + ); + } + } + } + + private function validateMemsCount(Context $c): void + { + $this->addErrorUnless( + count($c->mems) <= 1, + 'multiple memories are not allowed', + ); + } + + private function validateExportsHaveDifferentNames(): void + { + $names = []; + foreach ($this->module->exports as $export) { + if (in_array($export->name, $names, true)) { + $this->addError("Duplicate export name: {$export->name}"); + } + $names[] = $export->name; + } + } + + private function validateFunc(Context $c, Func $func): void + { + $funcType = $this->module->types[$func->type] ?? null; + if ($funcType === null) { + $this->addError("Invalid function type index: {$func->type}"); + return; + } + + $localTypes = array_merge( + $funcType->params, + array_map(fn ($l) => $l->type, $func->locals), + ); + + $funcContext = new Context( + types: $c->types, + funcs: $c->funcs, + tables: $c->tables, + mems: $c->mems, + globals: $c->globals, + elems: $c->elems, + datas: $c->datas, + locals: $localTypes, + labels: [$funcType->results], + return: $funcType->results, + refs: $c->refs, + ); + + $validationStack = new ValidationStack(); + $validationStack->pushCtrl(Instrs\Control\Block::class, [], $funcType->results); + + $functionValidator = new FunctionValidator($funcContext, $validationStack); + $errors = $functionValidator->validate($func->body); + array_push($this->errors, ...$errors); + } + + /** + * Collect the set of function indices that are "declared" (appear in elem segment inits or exports). + * + * @return list<int> + */ + private function collectRefs(): array + { + $refs = []; + foreach ($this->module->elems as $elem) { + foreach ($elem->init as $initExpr) { + foreach ($initExpr as $instr) { + if ($instr instanceof Instrs\Reference\RefFunc) { + $refs[] = $instr->func; + } + } + } + } + foreach ($this->module->exports as $export) { + if ($export->desc instanceof ExportDescs\Func) { + $refs[] = $export->desc->func; + } + } + return array_values(array_unique($refs)); + } + + private function addErrorUnless(bool $x, string $message): void + { + if (! $x) { + $this->addError($message); + } + } + + private function addError(string $message): void + { + $this->errors[] = $message; + } +} diff --git a/tests/src/SpecTestsuites/SpecTestsuiteBase.php b/tests/src/SpecTestsuites/SpecTestsuiteBase.php index 7106407..bcf7fa0 100644 --- a/tests/src/SpecTestsuites/SpecTestsuiteBase.php +++ b/tests/src/SpecTestsuites/SpecTestsuiteBase.php @@ -31,6 +31,7 @@ use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\MemType; use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\Mut; use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\TableType; use Nsfisis\Waddiwasi\WebAssembly\Structure\Types\ValType; +use Nsfisis\Waddiwasi\WebAssembly\Validation\Validator; use PHPUnit\Framework\TestCase; use RuntimeException; use function count; @@ -157,8 +158,18 @@ abstract class SpecTestsuiteBase extends TestCase string $text, int $line, ): void { - // @todo Our implementation does not support "validation" step. - $this->assertTrue(true); + $filePath = __DIR__ . "/../../fixtures/spec_testsuites/core/{$filename}"; + $wasmBinaryStream = new FileStream($filePath); + try { + $module = (new Decoder($wasmBinaryStream))->decode(); + } catch (InvalidBinaryFormatException) { + // Malformed binary is also a form of invalid module. + $this->assertTrue(true); + return; + } + $result = (new Validator($module))->validate(); + // @todo Check error message. + $this->assertNotEmpty($result->errors, "validating {$filename} is expected to fail (at {$line})"); } protected function runAssertExhaustionCommand( |
