From b42dac9193cb8ac7f6d46c1b71ed01be1b591c09 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Wed, 22 Nov 2023 21:13:34 -0500 Subject: [PATCH 1/8] fix: Validate Circular Buffer Capacity --- .../circular-buffer/.meta/proof.ci.wat | 39 ++++++++++++++++--- .../circular-buffer/circular-buffer.spec.js | 11 ++++++ .../circular-buffer/circular-buffer.wat | 7 ++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/exercises/practice/circular-buffer/.meta/proof.ci.wat b/exercises/practice/circular-buffer/.meta/proof.ci.wat index 0101c41..b99fa55 100644 --- a/exercises/practice/circular-buffer/.meta/proof.ci.wat +++ b/exercises/practice/circular-buffer/.meta/proof.ci.wat @@ -5,13 +5,17 @@ (global $capacity (mut i32) (i32.const 0)) (global $i32Size i32 (i32.const 4)) - - ;; capacity: the number of elements to store - ;; elementSize: the size of the element to store in bytes - ;; Does not support resizing circular buffer. Wipes all data + ;; + ;; Initialize a circular buffer of i32s with a given capacity + ;; + ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 16,384 + ;; in order to fit in a single 64KiB WebAssembly page + ;; + ;; @returns {i32} 0 on success or -1 on error + ;; (func (export "init") (param $newCapacity i32) (result i32) ;; a WebAssembly page is 4096 bytes, so up to 1024 i32s - (if (i32.gt_s (local.get $newCapacity) (i32.const 1024)) (then + (if (i32.gt_s (local.get $newCapacity) (i32.const 16384)) (then (return (i32.const -1)) )) @@ -21,11 +25,21 @@ (i32.const 0) ) + ;; + ;; Clear the circular buffer + ;; (func (export "clear") (global.set $head (i32.const -1)) (global.set $tail (i32.const -1)) ) + ;; + ;; Add an element to the circular buffer + ;; + ;; @param {i32} elem - element to add to the circular buffer + ;; + ;; @returns {i32} 0 on success or -1 if full + ;; (func (export "write") (param $elem i32) (result i32) (local $temp i32) ;; Table has capacity of zero @@ -52,6 +66,14 @@ (i32.const 0) ) + ;; + ;; Add an element to the circular buffer, overwriting the oldest element + ;; if the buffer is full + ;; + ;; @param {i32} elem - element to add to the circular buffer + ;; + ;; @returns {i32} 0 on success or -1 if full (capacity of zero) + ;; (func (export "forceWrite") (param $elem i32) (result i32) (local $temp i32) ;; Table has capacity of zero @@ -78,7 +100,12 @@ (i32.const 0) ) - ;; Go-style error handling type (i32,i32) + ;; + ;; Read the oldest element from the circular buffer, if not empty + ;; + ;; @returns {i32} element on success or -1 if empty + ;; @returns {i32} status code set to 0 on success or -1 if empty + ;; (func (export "read") (result i32 i32) (local $result i32) diff --git a/exercises/practice/circular-buffer/circular-buffer.spec.js b/exercises/practice/circular-buffer/circular-buffer.spec.js index 468cba2..4ebda4a 100644 --- a/exercises/practice/circular-buffer/circular-buffer.spec.js +++ b/exercises/practice/circular-buffer/circular-buffer.spec.js @@ -143,4 +143,15 @@ describe("CircularBuffer", () => { expect(currentInstance.exports.read()).toEqual([4, 0]); expect(currentInstance.exports.read()).toEqual([-1, -1]); }); + + xtest("Should be able to grow up to a full 64KiB page", () => { + expect(currentInstance.exports.init(16384)).toEqual(0); + for (let i = 0; i < 16384; i++) { + expect(currentInstance.exports.write(1024)).toEqual(0); + } + }); + + xtest("init should fail if greater than full 64KiB page", () => { + expect(currentInstance.exports.init(16385)).toEqual(-1); + }); }); diff --git a/exercises/practice/circular-buffer/circular-buffer.wat b/exercises/practice/circular-buffer/circular-buffer.wat index 426f93d..d8d4fc5 100644 --- a/exercises/practice/circular-buffer/circular-buffer.wat +++ b/exercises/practice/circular-buffer/circular-buffer.wat @@ -1,12 +1,13 @@ (module - (memory 1) + ;; Cap the memory at a single 64KiB page + (memory 1 1) ;; Add globals here! ;; ;; Initialize a circular buffer of i32s with a given capacity ;; - ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 1024 - ;; in order to fit in a single WebAssembly page + ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 16,384 + ;; in order to fit in a single 64KiB WebAssembly page ;; ;; @returns {i32} 0 on success or -1 on error ;; From 46ceb460fab4b5e2fa89b97c64e1dd38fcfde336 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Wed, 22 Nov 2023 22:38:30 -0500 Subject: [PATCH 2/8] fix: cap memory in proof --- exercises/practice/circular-buffer/.meta/proof.ci.wat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/.meta/proof.ci.wat b/exercises/practice/circular-buffer/.meta/proof.ci.wat index b99fa55..e8119b6 100644 --- a/exercises/practice/circular-buffer/.meta/proof.ci.wat +++ b/exercises/practice/circular-buffer/.meta/proof.ci.wat @@ -1,5 +1,5 @@ (module - (memory 1) + (memory 1 1) (global $head (mut i32) (i32.const -1)) (global $tail (mut i32) (i32.const -1)) (global $capacity (mut i32) (i32.const 0)) From d11433ba033e8f4de77cd73e2858691fe07b7742 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Wed, 22 Nov 2023 23:09:26 -0500 Subject: [PATCH 3/8] refactor: Remove suspicious power of 2 number --- exercises/practice/circular-buffer/circular-buffer.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/circular-buffer.spec.js b/exercises/practice/circular-buffer/circular-buffer.spec.js index 4ebda4a..726fac5 100644 --- a/exercises/practice/circular-buffer/circular-buffer.spec.js +++ b/exercises/practice/circular-buffer/circular-buffer.spec.js @@ -147,7 +147,7 @@ describe("CircularBuffer", () => { xtest("Should be able to grow up to a full 64KiB page", () => { expect(currentInstance.exports.init(16384)).toEqual(0); for (let i = 0; i < 16384; i++) { - expect(currentInstance.exports.write(1024)).toEqual(0); + expect(currentInstance.exports.write(i)).toEqual(0); } }); From 6bd4db7025074aa806b07208c51f370e74360ae2 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Fri, 24 Nov 2023 18:30:00 -0500 Subject: [PATCH 4/8] Update exercises/practice/circular-buffer/.meta/proof.ci.wat Co-authored-by: Glenn Jackman --- exercises/practice/circular-buffer/.meta/proof.ci.wat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/.meta/proof.ci.wat b/exercises/practice/circular-buffer/.meta/proof.ci.wat index e8119b6..50259da 100644 --- a/exercises/practice/circular-buffer/.meta/proof.ci.wat +++ b/exercises/practice/circular-buffer/.meta/proof.ci.wat @@ -14,7 +14,7 @@ ;; @returns {i32} 0 on success or -1 on error ;; (func (export "init") (param $newCapacity i32) (result i32) - ;; a WebAssembly page is 4096 bytes, so up to 1024 i32s + ;; a WebAssembly page is 64KiB, so up to 16384 i32s (if (i32.gt_s (local.get $newCapacity) (i32.const 16384)) (then (return (i32.const -1)) )) From fc719e220bd14598d7fe6cf3736a7ae6acb1a9b8 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Fri, 24 Nov 2023 19:32:43 -0500 Subject: [PATCH 5/8] feat: Circular Buffer can grow up to four pages --- .../practice/circular-buffer/.docs/hints.md | 8 +++ .../circular-buffer/.meta/proof.ci.wat | 32 +++++++--- .../circular-buffer/circular-buffer.spec.js | 63 ++++++++++++++++--- .../circular-buffer/circular-buffer.wat | 11 ++-- 4 files changed, 93 insertions(+), 21 deletions(-) diff --git a/exercises/practice/circular-buffer/.docs/hints.md b/exercises/practice/circular-buffer/.docs/hints.md index cfe36b6..36e9936 100644 --- a/exercises/practice/circular-buffer/.docs/hints.md +++ b/exercises/practice/circular-buffer/.docs/hints.md @@ -1,3 +1,11 @@ # Hints Linear memory is byte-addressable, but `i32` has a width of four bytes. + +"The `memory.grow` instruction is used to grow a linear memory. The instruction grows memory by a given delta and returns the previous size, or -1 if enough memory cannot be allocated. Both instructions operate in units of page size." + +From https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-memory + +"A memory instance... holds a vector of bytes. The length of the vector always is a multiple of the WebAssembly page size, which is defined to be the constant 65536" + +From https://webassembly.github.io/spec/core/exec/runtime.html#page-size diff --git a/exercises/practice/circular-buffer/.meta/proof.ci.wat b/exercises/practice/circular-buffer/.meta/proof.ci.wat index 50259da..4e8dc20 100644 --- a/exercises/practice/circular-buffer/.meta/proof.ci.wat +++ b/exercises/practice/circular-buffer/.meta/proof.ci.wat @@ -1,5 +1,9 @@ (module - (memory 1 1) + ;; a WebAssembly page is 64KiB, so each page holds up to 16384 i32s + ;; Our linear memory is one page by default, but it is permitted to grow + ;; up to four pages via use of the memory.grow instruction, which can hold + ;; up to 65536 i32s. + (memory (export "mem") 1 4) (global $head (mut i32) (i32.const -1)) (global $tail (mut i32) (i32.const -1)) (global $capacity (mut i32) (i32.const 0)) @@ -8,21 +12,31 @@ ;; ;; Initialize a circular buffer of i32s with a given capacity ;; - ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 16,384 - ;; in order to fit in a single 64KiB WebAssembly page + ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 65,536 + ;; in order to fit in four 64KiB WebAssembly pages. ;; ;; @returns {i32} 0 on success or -1 on error ;; (func (export "init") (param $newCapacity i32) (result i32) - ;; a WebAssembly page is 64KiB, so up to 16384 i32s - (if (i32.gt_s (local.get $newCapacity) (i32.const 16384)) (then - (return (i32.const -1)) - )) + ;; a WebAssembly page is 64KiB, so each page holds up to 16384 i32s + ;; Our linear memory can grow up to four pages, so we can hold up to 65536 i32s + (if (i32.or + (i32.lt_s (local.get $newCapacity) (i32.const 0)) + (i32.gt_s (local.get $newCapacity) (i32.const 65536))) (then + (return (i32.const -1)))) (global.set $head (i32.const -1)) (global.set $tail (i32.const -1)) (global.set $capacity (local.get $newCapacity)) - (i32.const 0) + + ;; We do not need to grow the memory if the new capacity is less than 16384 + (if (result i32) (i32.le_s (local.get $newCapacity) (i32.const 16384)) (then + (i32.const 0) + ) (else + ;; memory.grow returns old size on success or -1 on failure + (memory.grow (i32.div_s (i32.sub (local.get $newCapacity) (i32.const 1)) (i32.const 16384))) + (if (result i32) (i32.ne (i32.const -1)) (i32.const 0) (i32.const -1)) + )) ) ;; @@ -126,4 +140,4 @@ (global.set $head (i32.rem_u (i32.add (global.get $head) (i32.const 1)) (global.get $capacity))) (return (local.get $result) (i32.const 0)) ) -) +) \ No newline at end of file diff --git a/exercises/practice/circular-buffer/circular-buffer.spec.js b/exercises/practice/circular-buffer/circular-buffer.spec.js index 726fac5..5fa8792 100644 --- a/exercises/practice/circular-buffer/circular-buffer.spec.js +++ b/exercises/practice/circular-buffer/circular-buffer.spec.js @@ -30,7 +30,55 @@ describe("CircularBuffer", () => { } }); - test("reading empty buffer should fail", () => { + test("initializing negative capacity should error", () => { + expect(currentInstance.exports.init(-1)).toEqual(-1); + }); + + xtest("initializing capacity of 0 i32s should result in a linear memory with 1 page", () => { + expect(currentInstance.exports.init(0)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(1); + }); + + xtest("initializing capacity of 16384 i32s should result in a linear memory with 1 page", () => { + expect(currentInstance.exports.init(16384)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(1); + }); + + xtest("initializing capacity of 16385 i32s should result in a linear memory with 2 pages", () => { + expect(currentInstance.exports.init(16385)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(2); + }); + + xtest("initializing capacity of 32768 i32s should result in a linear memory with 2 pages", () => { + expect(currentInstance.exports.init(32768)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(2); + }); + + xtest("initializing capacity of 32769 i32s should result in a linear memory with 3 pages", () => { + expect(currentInstance.exports.init(32769)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(3); + }); + + xtest("initializing capacity of 49152 i32s should result in a linear memory with 3 pages", () => { + expect(currentInstance.exports.init(49152)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(3); + }); + + xtest("initializing capacity of 49153 i32s should result in a linear memory with 4 pages", () => { + expect(currentInstance.exports.init(49153)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(4); + }); + + xtest("initializing capacity of 65536 i32s should result in a linear memory with 4 pages", () => { + expect(currentInstance.exports.init(65536)).toEqual(0); + expect(currentInstance.exports.mem.buffer.byteLength / 65536).toEqual(4); + }); + + xtest("initializing capacity greater than 65536 should error", () => { + expect(currentInstance.exports.init(65537)).toEqual(-1); + }); + + xtest("reading empty buffer should fail", () => { expect(currentInstance.exports.init(1)).toEqual(0); expect(currentInstance.exports.read()).toEqual([-1, -1]); }); @@ -144,14 +192,13 @@ describe("CircularBuffer", () => { expect(currentInstance.exports.read()).toEqual([-1, -1]); }); - xtest("Should be able to grow up to a full 64KiB page", () => { - expect(currentInstance.exports.init(16384)).toEqual(0); - for (let i = 0; i < 16384; i++) { + xtest("Should be able to write and read up to the full capacity of four 64Kib pages", () => { + expect(currentInstance.exports.init(65536)).toEqual(0); + for (let i = 0; i < 65536; i++) { expect(currentInstance.exports.write(i)).toEqual(0); } - }); - - xtest("init should fail if greater than full 64KiB page", () => { - expect(currentInstance.exports.init(16385)).toEqual(-1); + for (let i = 0; i < 65536; i++) { + expect(currentInstance.exports.read()).toEqual([i, 0]); + } }); }); diff --git a/exercises/practice/circular-buffer/circular-buffer.wat b/exercises/practice/circular-buffer/circular-buffer.wat index d8d4fc5..f6e089b 100644 --- a/exercises/practice/circular-buffer/circular-buffer.wat +++ b/exercises/practice/circular-buffer/circular-buffer.wat @@ -1,13 +1,16 @@ (module - ;; Cap the memory at a single 64KiB page - (memory 1 1) + ;; a WebAssembly page is 64KiB, so each page holds up to 16384 i32s + ;; Our linear memory is one page by default, but it is permitted to grow + ;; up to four pages via use of the memory.grow instruction, which can hold + ;; up to 65536 i32s. + (memory (export "mem") 1 4) ;; Add globals here! ;; ;; Initialize a circular buffer of i32s with a given capacity ;; - ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 16,384 - ;; in order to fit in a single 64KiB WebAssembly page + ;; @param {i32} newCapacity - capacity of the circular buffer between 0 and 65,536 + ;; in order to fit in four 64KiB WebAssembly pages. ;; ;; @returns {i32} 0 on success or -1 on error ;; From 60e16a254156e7e1d6d6c867a5ab06278223d997 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Sat, 25 Nov 2023 11:19:56 -0500 Subject: [PATCH 6/8] Update exercises/practice/circular-buffer/circular-buffer.wat Co-authored-by: Glenn Jackman --- exercises/practice/circular-buffer/circular-buffer.wat | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/circular-buffer/circular-buffer.wat b/exercises/practice/circular-buffer/circular-buffer.wat index f6e089b..0a97b9b 100644 --- a/exercises/practice/circular-buffer/circular-buffer.wat +++ b/exercises/practice/circular-buffer/circular-buffer.wat @@ -1,8 +1,8 @@ (module - ;; a WebAssembly page is 64KiB, so each page holds up to 16384 i32s - ;; Our linear memory is one page by default, but it is permitted to grow - ;; up to four pages via use of the memory.grow instruction, which can hold - ;; up to 65536 i32s. + ;; Linear memory is allocated one page by default. + ;; A page is 64KiB, and that can hold up to 16384 i32s. + ;; We will permit memory to grow to a maximum of four pages. + ;; The maximum capacity of our buffer is 65536 i32s. (memory (export "mem") 1 4) ;; Add globals here! From e425c5384231a17872e541b1abe8ebc7a470f649 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Sat, 25 Nov 2023 11:34:25 -0500 Subject: [PATCH 7/8] docs: Enhance hints for Circular Buffer --- exercises/practice/circular-buffer/.docs/hints.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/exercises/practice/circular-buffer/.docs/hints.md b/exercises/practice/circular-buffer/.docs/hints.md index 36e9936..e731b59 100644 --- a/exercises/practice/circular-buffer/.docs/hints.md +++ b/exercises/practice/circular-buffer/.docs/hints.md @@ -1,11 +1,16 @@ # Hints -Linear memory is byte-addressable, but `i32` has a width of four bytes. +`i32` has a width of four bytes. -"The `memory.grow` instruction is used to grow a linear memory. The instruction grows memory by a given delta and returns the previous size, or -1 if enough memory cannot be allocated. Both instructions operate in units of page size." +"A memory instance... holds a vector of bytes. The length of the vector always is a multiple of the WebAssembly page size, which is defined to be the constant 65536" -From https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-memory +[WebAssembly Specification: Memory Instances](https://webassembly.github.io/spec/core/exec/runtime.html#page-size) -"A memory instance... holds a vector of bytes. The length of the vector always is a multiple of the WebAssembly page size, which is defined to be the constant 65536" +"The `memory.size` instruction returns the current size of a memory. The `memory.grow` instruction grows memory by a given delta and returns the previous size, or -1 if enough memory cannot be allocated. Both instructions operate in units of page size." + +[WebAssembly Specification: Memory Instructions](https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-memory) + +Further References: -From https://webassembly.github.io/spec/core/exec/runtime.html#page-size +- [memory.size at MDN](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/Size) +- [memory.grow at MDN](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/Grow) From 79b8f7d8af098a61d484b88f52a965a6b776ca0d Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Sat, 25 Nov 2023 11:37:48 -0500 Subject: [PATCH 8/8] docs: Update linear memory comment in proof.ci.wat for circular buffer --- exercises/practice/circular-buffer/.meta/proof.ci.wat | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/circular-buffer/.meta/proof.ci.wat b/exercises/practice/circular-buffer/.meta/proof.ci.wat index 4e8dc20..d076243 100644 --- a/exercises/practice/circular-buffer/.meta/proof.ci.wat +++ b/exercises/practice/circular-buffer/.meta/proof.ci.wat @@ -1,8 +1,8 @@ (module - ;; a WebAssembly page is 64KiB, so each page holds up to 16384 i32s - ;; Our linear memory is one page by default, but it is permitted to grow - ;; up to four pages via use of the memory.grow instruction, which can hold - ;; up to 65536 i32s. + ;; Linear memory is allocated one page by default. + ;; A page is 64KiB, and that can hold up to 16384 i32s. + ;; We will permit memory to grow to a maximum of four pages. + ;; The maximum capacity of our buffer is 65536 i32s. (memory (export "mem") 1 4) (global $head (mut i32) (i32.const -1)) (global $tail (mut i32) (i32.const -1))