diff --git a/config.json b/config.json index 156f8f5..640f362 100644 --- a/config.json +++ b/config.json @@ -546,6 +546,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "variable-length-quantity", + "name": "Variable Length Quantity", + "uuid": "57031215-ee9e-4ccb-89e5-2502c83a109f", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "yacht", "name": "Yacht", diff --git a/exercises/practice/variable-length-quantity/.docs/instructions.md b/exercises/practice/variable-length-quantity/.docs/instructions.md new file mode 100644 index 0000000..5012548 --- /dev/null +++ b/exercises/practice/variable-length-quantity/.docs/instructions.md @@ -0,0 +1,34 @@ +# Instructions + +Implement variable length quantity encoding and decoding. + +The goal of this exercise is to implement [VLQ][vlq] encoding/decoding. + +In short, the goal of this encoding is to encode integer values in a way that would save bytes. +Only the first 7 bits of each byte are significant (right-justified; sort of like an ASCII byte). +So, if you have a 32-bit value, you have to unpack it into a series of 7-bit bytes. +Of course, you will have a variable number of bytes depending upon your integer. +To indicate which is the last byte of the series, you leave bit #7 clear. +In all of the preceding bytes, you set bit #7. + +So, if an integer is between `0-127`, it can be represented as one byte. +Although VLQ can deal with numbers of arbitrary sizes, for this exercise we will restrict ourselves to only numbers that fit in a 32-bit unsigned integer. +Here are examples of integers as 32-bit values, and the variable length quantities that they translate to: + +```text + NUMBER VARIABLE QUANTITY +00000000 00 +00000040 40 +0000007F 7F +00000080 81 00 +00002000 C0 00 +00003FFF FF 7F +00004000 81 80 00 +00100000 C0 80 00 +001FFFFF FF FF 7F +00200000 81 80 80 00 +08000000 C0 80 80 00 +0FFFFFFF FF FF FF 7F +``` + +[vlq]: https://en.wikipedia.org/wiki/Variable-length_quantity diff --git a/exercises/practice/variable-length-quantity/.meta/Example.lean b/exercises/practice/variable-length-quantity/.meta/Example.lean new file mode 100644 index 0000000..03b135d --- /dev/null +++ b/exercises/practice/variable-length-quantity/.meta/Example.lean @@ -0,0 +1,29 @@ +namespace VariableLengthQuantity + +private def mask : Nat := (1 <<< 7) - 1 +private def highBit : UInt8 := (1 <<< 7).toUInt8 + +def encode (integers : Array Nat) : ByteArray := + ByteArray.mk (integers.flatMap fun x => Id.run do + let mut acc := #[] + let mut num := x + repeat + let byte := num &&& mask + num := num >>> 7 + acc := acc.push (byte.toUInt8 ||| highBit) + until num == 0 + acc := acc.modify 0 (· &&& (~~~highBit)) -- array reversed, first byte is the last in sequence + return acc.reverse) + +def decode (bytes : ByteArray) : Option (Array Nat) := do + let mut acc := #[0] -- starts non-empty and pops at the end. Makes bit logic easier + for byte in bytes do + let masked := byte.toNat &&& mask + let last := acc.size - 1 + acc := acc.modify last (fun b => (b <<< 7) ||| masked) + if (byte &&& highBit) == 0 then acc := acc.push 0 + acc := acc.pop + guard !acc.isEmpty + return acc + +end VariableLengthQuantity diff --git a/exercises/practice/variable-length-quantity/.meta/config.json b/exercises/practice/variable-length-quantity/.meta/config.json new file mode 100644 index 0000000..223cfbd --- /dev/null +++ b/exercises/practice/variable-length-quantity/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "oxe-i" + ], + "files": { + "solution": [ + "VariableLengthQuantity.lean" + ], + "test": [ + "VariableLengthQuantityTest.lean" + ], + "example": [ + ".meta/Example.lean" + ] + }, + "blurb": "Implement variable length quantity encoding and decoding.", + "source": "A poor Splice developer having to implement MIDI encoding/decoding.", + "source_url": "https://splice.com" +} diff --git a/exercises/practice/variable-length-quantity/.meta/extra.json b/exercises/practice/variable-length-quantity/.meta/extra.json new file mode 100644 index 0000000..3d548bf --- /dev/null +++ b/exercises/practice/variable-length-quantity/.meta/extra.json @@ -0,0 +1,18 @@ +[ + { + "description": "Encode a series of integers, producing a series of bytes. -> big number", + "property": "encode", + "input": { + "integers": [6734734192409283617213650083744412813817389736008238494387248] + }, + "expected": [195, 135, 158, 227, 201, 169, 168, 216, 243, 217, 150, 229, 249, 158, 219, 184, 135, 139, 172, 205, 232, 158, 153, 252, 230, 222, 176, 152, 48] + }, + { + "description": "Decode a series of bytes, producing a series of integers. -> big number", + "property": "decode", + "input": { + "integers": [139, 199, 214, 207, 165, 200, 207, 193, 165, 250, 150, 183, 216, 207, 207, 230, 231, 162, 211, 239, 212, 240, 214, 222, 250, 18] + }, + "expected": [553612784957720002932751723682307655243628583457373458] + } +] diff --git a/exercises/practice/variable-length-quantity/.meta/tests.toml b/exercises/practice/variable-length-quantity/.meta/tests.toml new file mode 100644 index 0000000..53be789 --- /dev/null +++ b/exercises/practice/variable-length-quantity/.meta/tests.toml @@ -0,0 +1,103 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[35c9db2e-f781-4c52-b73b-8e76427defd0] +description = "Encode a series of integers, producing a series of bytes. -> zero" + +[be44d299-a151-4604-a10e-d4b867f41540] +description = "Encode a series of integers, producing a series of bytes. -> arbitrary single byte" + +[890bc344-cb80-45af-b316-6806a6971e81] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric single byte" + +[ea399615-d274-4af6-bbef-a1c23c9e1346] +description = "Encode a series of integers, producing a series of bytes. -> largest single byte" + +[77b07086-bd3f-4882-8476-8dcafee79b1c] +description = "Encode a series of integers, producing a series of bytes. -> smallest double byte" + +[63955a49-2690-4e22-a556-0040648d6b2d] +description = "Encode a series of integers, producing a series of bytes. -> arbitrary double byte" + +[4977d113-251b-4d10-a3ad-2f5a7756bb58] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric double byte" + +[29da7031-0067-43d3-83a7-4f14b29ed97a] +description = "Encode a series of integers, producing a series of bytes. -> largest double byte" + +[3345d2e3-79a9-4999-869e-d4856e3a8e01] +description = "Encode a series of integers, producing a series of bytes. -> smallest triple byte" + +[5df0bc2d-2a57-4300-a653-a75ee4bd0bee] +description = "Encode a series of integers, producing a series of bytes. -> arbitrary triple byte" + +[6731045f-1e00-4192-b5ae-98b22e17e9f7] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric triple byte" + +[f51d8539-312d-4db1-945c-250222c6aa22] +description = "Encode a series of integers, producing a series of bytes. -> largest triple byte" + +[da78228b-544f-47b7-8bfe-d16b35bbe570] +description = "Encode a series of integers, producing a series of bytes. -> smallest quadruple byte" + +[11ed3469-a933-46f1-996f-2231e05d7bb6] +description = "Encode a series of integers, producing a series of bytes. -> arbitrary quadruple byte" + +[b45ef770-cbba-48c2-bd3c-c6362679516e] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric quadruple byte" + +[d5f3f3c3-e0f1-4e7f-aad0-18a44f223d1c] +description = "Encode a series of integers, producing a series of bytes. -> largest quadruple byte" + +[91a18b33-24e7-4bfb-bbca-eca78ff4fc47] +description = "Encode a series of integers, producing a series of bytes. -> smallest quintuple byte" + +[5f34ff12-2952-4669-95fe-2d11b693d331] +description = "Encode a series of integers, producing a series of bytes. -> arbitrary quintuple byte" + +[9be46731-7cd5-415c-b960-48061cbc1154] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric quintuple byte" + +[7489694b-88c3-4078-9864-6fe802411009] +description = "Encode a series of integers, producing a series of bytes. -> maximum 32-bit integer input" + +[f9b91821-cada-4a73-9421-3c81d6ff3661] +description = "Encode a series of integers, producing a series of bytes. -> two single-byte values" + +[68694449-25d2-4974-ba75-fa7bb36db212] +description = "Encode a series of integers, producing a series of bytes. -> two multi-byte values" + +[51a06b5c-de1b-4487-9a50-9db1b8930d85] +description = "Encode a series of integers, producing a series of bytes. -> many multi-byte values" + +[baa73993-4514-4915-bac0-f7f585e0e59a] +description = "Decode a series of bytes, producing a series of integers. -> one byte" + +[72e94369-29f9-46f2-8c95-6c5b7a595aee] +description = "Decode a series of bytes, producing a series of integers. -> two bytes" + +[df5a44c4-56f7-464e-a997-1db5f63ce691] +description = "Decode a series of bytes, producing a series of integers. -> three bytes" + +[1bb58684-f2dc-450a-8406-1f3452aa1947] +description = "Decode a series of bytes, producing a series of integers. -> four bytes" + +[cecd5233-49f1-4dd1-a41a-9840a40f09cd] +description = "Decode a series of bytes, producing a series of integers. -> maximum 32-bit integer" + +[e7d74ba3-8b8e-4bcb-858d-d08302e15695] +description = "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error" + +[aa378291-9043-4724-bc53-aca1b4a3fcb6] +description = "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error, even if value is zero" + +[a91e6f5a-c64a-48e3-8a75-ce1a81e0ebee] +description = "Decode a series of bytes, producing a series of integers. -> multiple values" diff --git a/exercises/practice/variable-length-quantity/VariableLengthQuantity.lean b/exercises/practice/variable-length-quantity/VariableLengthQuantity.lean new file mode 100644 index 0000000..03d8fba --- /dev/null +++ b/exercises/practice/variable-length-quantity/VariableLengthQuantity.lean @@ -0,0 +1,9 @@ +namespace VariableLengthQuantity + +def encode (integers : Array Nat) : ByteArray := + sorry --remove this line and implement the function + +def decode (bytes : ByteArray) : Option (Array Nat) := + sorry --remove this line and implement the function + +end VariableLengthQuantity diff --git a/exercises/practice/variable-length-quantity/VariableLengthQuantityTest.lean b/exercises/practice/variable-length-quantity/VariableLengthQuantityTest.lean new file mode 100644 index 0000000..5df4520 --- /dev/null +++ b/exercises/practice/variable-length-quantity/VariableLengthQuantityTest.lean @@ -0,0 +1,454 @@ +import LeanTest +import VariableLengthQuantity + +open LeanTest + +deriving instance Repr for ByteArray + +def variableLengthQuantityTests : TestSuite := + (TestSuite.empty "VariableLengthQuantity") + |>.addTest "Encode a series of integers, producing a series of bytes. -> zero" (do + let expected : ByteArray := ByteArray.mk #[ + 0x00 + ] + let input : Array Nat := #[ + 0 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> arbitrary single byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x40 + ] + let input : Array Nat := #[ + 64 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> asymmetric single byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x53 + ] + let input : Array Nat := #[ + 83 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> largest single byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x7f + ] + let input : Array Nat := #[ + 127 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> smallest double byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x00 + ] + let input : Array Nat := #[ + 128 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> arbitrary double byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xc0, + 0x00 + ] + let input : Array Nat := #[ + 8192 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> asymmetric double byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x2d + ] + let input : Array Nat := #[ + 173 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> largest double byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xff, + 0x7f + ] + let input : Array Nat := #[ + 16383 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> smallest triple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 16384 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> arbitrary triple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xc0, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 1048576 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> asymmetric triple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x87, + 0xab, + 0x1c + ] + let input : Array Nat := #[ + 120220 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> largest triple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xff, + 0xff, + 0x7f + ] + let input : Array Nat := #[ + 2097151 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> smallest quadruple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x80, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 2097152 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> arbitrary quadruple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xc0, + 0x80, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 134217728 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> asymmetric quadruple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0xd5, + 0xee, + 0x04 + ] + let input : Array Nat := #[ + 3503876 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> largest quadruple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0xff, + 0xff, + 0xff, + 0x7f + ] + let input : Array Nat := #[ + 268435455 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> smallest quintuple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x80, + 0x80, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 268435456 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> arbitrary quintuple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x8f, + 0xf8, + 0x80, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 4278190080 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> asymmetric quintuple byte" (do + let expected : ByteArray := ByteArray.mk #[ + 0x88, + 0xb3, + 0x95, + 0xc2, + 0x05 + ] + let input : Array Nat := #[ + 2254790917 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> maximum 32-bit integer input" (do + let expected : ByteArray := ByteArray.mk #[ + 0x8f, + 0xff, + 0xff, + 0xff, + 0x7f + ] + let input : Array Nat := #[ + 4294967295 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> two single-byte values" (do + let expected : ByteArray := ByteArray.mk #[ + 0x40, + 0x7f + ] + let input : Array Nat := #[ + 64, + 127 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> two multi-byte values" (do + let expected : ByteArray := ByteArray.mk #[ + 0x81, + 0x80, + 0x00, + 0xc8, + 0xe8, + 0x56 + ] + let input : Array Nat := #[ + 16384, + 1193046 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> many multi-byte values" (do + let expected : ByteArray := ByteArray.mk #[ + 0xc0, + 0x00, + 0xc8, + 0xe8, + 0x56, + 0xff, + 0xff, + 0xff, + 0x7f, + 0x00, + 0xff, + 0x7f, + 0x81, + 0x80, + 0x00 + ] + let input : Array Nat := #[ + 8192, + 1193046, + 268435455, + 0, + 16383, + 16384 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> one byte" (do + let expected : Option (Array Nat) := some #[ + 127 + ] + let input : ByteArray := ByteArray.mk #[ + 0x7f + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> two bytes" (do + let expected : Option (Array Nat) := some #[ + 8192 + ] + let input : ByteArray := ByteArray.mk #[ + 0xc0, + 0x00 + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> three bytes" (do + let expected : Option (Array Nat) := some #[ + 2097151 + ] + let input : ByteArray := ByteArray.mk #[ + 0xff, + 0xff, + 0x7f + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> four bytes" (do + let expected : Option (Array Nat) := some #[ + 2097152 + ] + let input : ByteArray := ByteArray.mk #[ + 0x81, + 0x80, + 0x80, + 0x00 + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> maximum 32-bit integer" (do + let expected : Option (Array Nat) := some #[ + 4294967295 + ] + let input : ByteArray := ByteArray.mk #[ + 0x8f, + 0xff, + 0xff, + 0xff, + 0x7f + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error" (do + let expected : Option (Array Nat) := none + let input : ByteArray := ByteArray.mk #[ + 0xff + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error, even if value is zero" (do + let expected : Option (Array Nat) := none + let input : ByteArray := ByteArray.mk #[ + 0x80 + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> multiple values" (do + let expected : Option (Array Nat) := some #[ + 8192, + 1193046, + 268435455, + 0, + 16383, + 16384 + ] + let input : ByteArray := ByteArray.mk #[ + 0xc0, + 0x00, + 0xc8, + 0xe8, + 0x56, + 0xff, + 0xff, + 0xff, + 0x7f, + 0x00, + 0xff, + 0x7f, + 0x81, + 0x80, + 0x00 + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + |>.addTest "Encode a series of integers, producing a series of bytes. -> big number" (do + let expected : ByteArray := ByteArray.mk #[ + 0xc3, + 0x87, + 0x9e, + 0xe3, + 0xc9, + 0xa9, + 0xa8, + 0xd8, + 0xf3, + 0xd9, + 0x96, + 0xe5, + 0xf9, + 0x9e, + 0xdb, + 0xb8, + 0x87, + 0x8b, + 0xac, + 0xcd, + 0xe8, + 0x9e, + 0x99, + 0xfc, + 0xe6, + 0xde, + 0xb0, + 0x98, + 0x30 + ] + let input : Array Nat := #[ + 6734734192409283617213650083744412813817389736008238494387248 + ] + let actual : ByteArray := VariableLengthQuantity.encode input + return assertEqual expected actual) + |>.addTest "Decode a series of bytes, producing a series of integers. -> big number" (do + let expected : Option (Array Nat) := some #[ + 553612784957720002932751723682307655243628583457373458 + ] + let input : ByteArray := ByteArray.mk #[ + 0x8b, + 0xc7, + 0xd6, + 0xcf, + 0xa5, + 0xc8, + 0xcf, + 0xc1, + 0xa5, + 0xfa, + 0x96, + 0xb7, + 0xd8, + 0xcf, + 0xcf, + 0xe6, + 0xe7, + 0xa2, + 0xd3, + 0xef, + 0xd4, + 0xf0, + 0xd6, + 0xde, + 0xfa, + 0x12 + ] + let actual : Option (Array Nat) := VariableLengthQuantity.decode input + return assertEqual expected actual) + +def main : IO UInt32 := do + runTestSuitesWithExitCode [variableLengthQuantityTests] diff --git a/exercises/practice/variable-length-quantity/lakefile.toml b/exercises/practice/variable-length-quantity/lakefile.toml new file mode 100644 index 0000000..9cdad27 --- /dev/null +++ b/exercises/practice/variable-length-quantity/lakefile.toml @@ -0,0 +1,15 @@ +name = "variable-length-quantity" +version = "0.1.0" +defaultTargets = ["VariableLengthQuantityTest"] +testDriver = "VariableLengthQuantityTest" + +[[lean_lib]] +name = "LeanTest" +srcDir = "vendor/LeanTest" + +[[lean_lib]] +name = "VariableLengthQuantity" + +[[lean_exe]] +name = "VariableLengthQuantityTest" +root = "VariableLengthQuantityTest" diff --git a/exercises/practice/variable-length-quantity/lean-toolchain b/exercises/practice/variable-length-quantity/lean-toolchain new file mode 100644 index 0000000..14791d7 --- /dev/null +++ b/exercises/practice/variable-length-quantity/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.29.0 diff --git a/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest.lean b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest.lean new file mode 100644 index 0000000..012ba20 --- /dev/null +++ b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest.lean @@ -0,0 +1,4 @@ +-- This module serves as the root of the `LeanTest` library. +-- Import modules here that should be built as part of the library. +import LeanTest.Assertions +import LeanTest.Test diff --git a/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Assertions.lean b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Assertions.lean new file mode 100644 index 0000000..60e4ad8 --- /dev/null +++ b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Assertions.lean @@ -0,0 +1,166 @@ +/- +Assertion functions for unit testing. +-/ + +namespace LeanTest + +/-- Result of a test assertion -/ +inductive AssertionResult where + | success : AssertionResult + | failure (message : String) : AssertionResult + deriving Repr, BEq + +namespace AssertionResult + +def isSuccess : AssertionResult → Bool + | success => true + | failure _ => false + +def getMessage : AssertionResult → String + | success => "Assertion passed" + | failure msg => msg + +end AssertionResult + +/-- Assert that a boolean condition is true -/ +def assert (condition : Bool) (message : String := "Assertion failed") : AssertionResult := + if condition then + .success + else + .failure message + +/-- Assert that two values are equal -/ +def assertEqual [BEq α] [Repr α] (expected : α) (actual : α) (message : String := "") : AssertionResult := + if expected == actual then + .success + else + let msg := if message.isEmpty then + s!"Expected: {repr expected}\nActual: {repr actual}" + else + s!"{message}\nExpected: {repr expected}\nActual: {repr actual}" + .failure msg + +/-- Assert that two values are not equal -/ +def assertNotEqual [BEq α] [Repr α] (expected : α) (actual : α) (message : String := "") : AssertionResult := + if expected != actual then + .success + else + let msg := if message.isEmpty then + s!"Expected values to be different, but both were: {repr expected}" + else + s!"{message}\nExpected values to be different, but both were: {repr expected}" + .failure msg + +/-- Refute that a boolean condition is true (assert it's false) -/ +def refute (condition : Bool) (message : String := "Refute failed - condition was true") : AssertionResult := + if !condition then + .success + else + .failure message + +/-- Assert that a value is true -/ +def assertTrue (value : Bool) (message : String := "Expected true but got false") : AssertionResult := + assert value message + +/-- Assert that a value is false -/ +def assertFalse (value : Bool) (message : String := "Expected false but got true") : AssertionResult := + refute value message + +/-- Assert that an Option is some -/ +def assertSome [Repr α] (opt : Option α) (message : String := "Expected Some but got None") : AssertionResult := + match opt with + | some _ => .success + | none => .failure message + +/-- Assert that an Option is none -/ +def assertNone [Repr α] (opt : Option α) (message : String := "") : AssertionResult := + match opt with + | none => .success + | some val => + let msg := if message.isEmpty then + s!"Expected None but got Some: {repr val}" + else + s!"{message}\nExpected None but got Some: {repr val}" + .failure msg + +/-- Assert that a list is empty -/ +def assertEmpty [Repr α] (list : List α) (message : String := "") : AssertionResult := + match list with + | [] => .success + | _ => + let msg := if message.isEmpty then + s!"Expected empty list but got: {repr list}" + else + s!"{message}\nExpected empty list but got: {repr list}" + .failure msg + +/-- Assert that a list contains an element -/ +def assertContains [BEq α] [Repr α] (list : List α) (element : α) (message : String := "") : AssertionResult := + if list.contains element then + .success + else + let msg := if message.isEmpty then + s!"Expected list to contain {repr element}\nList: {repr list}" + else + s!"{message}\nExpected list to contain {repr element}\nList: {repr list}" + .failure msg + +/-- Assert that a value is within a range (inclusive) -/ +def assertInRange [LE α] [DecidableRel (· ≤ · : α → α → Prop)] [Repr α] + (value : α) (min : α) (max : α) (message : String := "") : AssertionResult := + if min ≤ value ∧ value ≤ max then + .success + else + let msg := if message.isEmpty then + s!"Expected {repr value} to be in range [{repr min}, {repr max}]" + else + s!"{message}\nExpected {repr value} to be in range [{repr min}, {repr max}]" + .failure msg + +/-- Assert that an Except value is an error -/ +def assertError [Repr ε] [Repr α] (result : Except ε α) (message : String := "") : AssertionResult := + match result with + | .error _ => .success + | .ok val => + let msg := if message.isEmpty then + s!"Expected error but got Ok: {repr val}" + else + s!"{message}\nExpected error but got Ok: {repr val}" + .failure msg + +/-- Assert that an Except value is ok -/ +def assertOk [Repr ε] [Repr α] (result : Except ε α) (message : String := "") : AssertionResult := + match result with + | .ok _ => .success + | .error err => + let msg := if message.isEmpty then + s!"Expected Ok but got error: {repr err}" + else + s!"{message}\nExpected Ok but got error: {repr err}" + .failure msg + +/-- Assert that an IO action throws an error -/ +def assertThrows (action : IO α) (message : String := "") : IO AssertionResult := do + try + let _ ← action + let msg := if message.isEmpty then + "Expected IO action to throw an error, but it succeeded" + else + s!"{message}\nExpected IO action to throw an error, but it succeeded" + return .failure msg + catch _ => + return .success + +/-- Assert that an IO action succeeds (doesn't throw) -/ +def assertSucceeds (action : IO α) (message : String := "") : IO AssertionResult := do + try + let _ ← action + return .success + catch e => + let msg := if message.isEmpty then + s!"Expected IO action to succeed, but it threw: {e}" + else + s!"{message}\nExpected IO action to succeed, but it threw: {e}" + return .failure msg + +end LeanTest diff --git a/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Test.lean b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Test.lean new file mode 100644 index 0000000..5ddbae5 --- /dev/null +++ b/exercises/practice/variable-length-quantity/vendor/LeanTest/LeanTest/Test.lean @@ -0,0 +1,130 @@ +/- +Test case and test suite management. +-/ + +import LeanTest.Assertions + +namespace LeanTest + +/-- A single test case -/ +structure TestCase where + description : String + test : IO AssertionResult + deriving Inhabited + +/-- Result of running a test -/ +structure TestResult where + description : String + result : AssertionResult + deriving Repr + +/-- A collection of tests (test suite) -/ +structure TestSuite where + name : String + tests : List TestCase + deriving Inhabited + +namespace TestSuite + +/-- Create an empty test suite -/ +def empty (name : String) : TestSuite := + { name := name, tests := [] } + +/-- Add a test to the suite -/ +def addTest (suite : TestSuite) (description : String) (test : IO AssertionResult) : TestSuite := + { suite with tests := suite.tests ++ [{ description := description, test := test }] } + +end TestSuite + +/-- Test statistics -/ +structure TestStats where + total : Nat + passed : Nat + failed : Nat + deriving Repr + +namespace TestStats + +def empty : TestStats := + { total := 0, passed := 0, failed := 0 } + +def addResult (stats : TestStats) (result : AssertionResult) : TestStats := + { total := stats.total + 1 + , passed := if result.isSuccess then stats.passed + 1 else stats.passed + , failed := if result.isSuccess then stats.failed else stats.failed + 1 + } + +end TestStats + +/-- ANSI color codes for terminal output -/ +def greenColor : String := "\x1b[32m" +def redColor : String := "\x1b[31m" +def yellowColor : String := "\x1b[33m" +def resetColor : String := "\x1b[0m" +def boldColor : String := "\x1b[1m" + +/-- Run a single test and print the result -/ +def runTest (testCase : TestCase) : IO TestResult := do + let result ← testCase.test + match result with + | .success => + IO.println s!" {greenColor}✓{resetColor} {testCase.description}" + | .failure msg => + IO.println s!" {redColor}✗{resetColor} {testCase.description}" + IO.println s!" {redColor}{msg}{resetColor}" + return { description := testCase.description, result := result } + +/-- Run all tests in a test suite -/ +def runTestSuite (suite : TestSuite) : IO TestStats := do + IO.println s!"\n{boldColor}{suite.name}{resetColor}" + let mut stats := TestStats.empty + + for testCase in suite.tests do + let result ← runTest testCase + stats := stats.addResult result.result + + return stats + +/-- Print test summary -/ +def printSummary (stats : TestStats) : IO Unit := do + IO.println "" + IO.println s!"{boldColor}Test Summary:{resetColor}" + IO.println s!" Total: {stats.total}" + IO.println s!" {greenColor}Passed: {stats.passed}{resetColor}" + + if stats.failed > 0 then + IO.println s!" {redColor}Failed: {stats.failed}{resetColor}" + IO.println s!"\n{redColor}FAILED{resetColor}" + else + IO.println s!"\n{greenColor}ALL TESTS PASSED{resetColor}" + +/-- Run multiple test suites -/ +def runTestSuites (suites : List TestSuite) : IO Unit := do + let mut totalStats := TestStats.empty + + for suite in suites do + let stats ← runTestSuite suite + totalStats := { + total := totalStats.total + stats.total, + passed := totalStats.passed + stats.passed, + failed := totalStats.failed + stats.failed + } + + printSummary totalStats + +/-- Run multiple test suites and return exit code (0 = all passed, 1 = some failed) -/ +def runTestSuitesWithExitCode (suites : List TestSuite) : IO UInt32 := do + let mut totalStats := TestStats.empty + + for suite in suites do + let stats ← runTestSuite suite + totalStats := { + total := totalStats.total + stats.total, + passed := totalStats.passed + stats.passed, + failed := totalStats.failed + stats.failed + } + + printSummary totalStats + return if totalStats.failed > 0 then 1 else 0 + +end LeanTest diff --git a/generators/Generator/Generator.lean b/generators/Generator/Generator.lean index ceb8e7f..19d3888 100644 --- a/generators/Generator/Generator.lean +++ b/generators/Generator/Generator.lean @@ -1,3 +1,4 @@ +import Generator.VariableLengthQuantityGenerator import Generator.ReverseListGenerator import Generator.ResistorColorGenerator import Generator.StrainGenerator @@ -103,6 +104,7 @@ abbrev endBodyGenerator := String -> String def dispatch : Std.HashMap String (introGenerator × testCaseGenerator × endBodyGenerator) := Std.HashMap.ofList [ + ("VariableLengthQuantity", (VariableLengthQuantityGenerator.genIntro, VariableLengthQuantityGenerator.genTestCase, VariableLengthQuantityGenerator.genEnd)), ("ReverseList", (ReverseListGenerator.genIntro, ReverseListGenerator.genTestCase, ReverseListGenerator.genEnd)), ("ResistorColor", (ResistorColorGenerator.genIntro, ResistorColorGenerator.genTestCase, ResistorColorGenerator.genEnd)), ("Strain", (StrainGenerator.genIntro, StrainGenerator.genTestCase, StrainGenerator.genEnd)), diff --git a/generators/Generator/Generator/VariableLengthQuantityGenerator.lean b/generators/Generator/Generator/VariableLengthQuantityGenerator.lean new file mode 100644 index 0000000..4599c9b --- /dev/null +++ b/generators/Generator/Generator/VariableLengthQuantityGenerator.lean @@ -0,0 +1,61 @@ +import Lean.Data.Json +import Std +import Helper + +open Lean +open Std +open Helper + +namespace VariableLengthQuantityGenerator + +def genIntro (exercise : String) : String := s!"import LeanTest +import {exercise} + +open LeanTest + +deriving instance Repr for ByteArray + +def {exercise.decapitalize}Tests : TestSuite := + (TestSuite.empty \"{exercise}\")" + +def serializeHex (num : Json) : String := + let byte := num.getNat? |> getOk + match Nat.toDigits 16 byte with + | [a, b] => s!"0x{a}{b}" + | [a] => s!"0x0{a}" + | _ => "0x00" + +def genTestCase (exercise : String) (case : TreeMap.Raw String Json) : String := + let input := case.get! "input" |>.getObjValD "integers" + let expected := case.get! "expected" + let description := case.get! "description" + |> (·.compress) + match getFunName (case.get! "property") with + | "encode" => + s!" + |>.addTest {description} (do + let expected : ByteArray := ByteArray.mk #{serializeList expected serializeHex} + let input : Array Nat := #{serializeList input} + let actual : ByteArray := {exercise}.encode input + return assertEqual expected actual)" + | "decode" => + let result := match expected.getObjVal? "error" with + | .error _ => s!"some #{serializeList expected}" + | .ok _ => "none" + let input := input + s!" + |>.addTest {description} (do + let expected : Option (Array Nat) := {result} + let input : ByteArray := ByteArray.mk #{serializeList input serializeHex} + let actual : Option (Array Nat) := {exercise}.decode input + return assertEqual expected actual)" + | _ => "" + +def genEnd (exercise : String) : String := + s!" + +def main : IO UInt32 := do + runTestSuitesWithExitCode [{exercise.decapitalize}Tests] +" + +end VariableLengthQuantityGenerator diff --git a/generators/Generator/Helper.lean b/generators/Generator/Helper.lean index e66fec3..168fd76 100644 --- a/generators/Generator/Helper.lean +++ b/generators/Generator/Helper.lean @@ -84,8 +84,8 @@ def serializeList : String := let contents := serializeContent json serializer match contents with - | [] => "[]" - | xs => + | [] => "[]" + | xs => let joined := String.intercalate separator xs s!"[\n{indent} {joined}\n{indent}]"