Current section
26 Versions
Jump to
Current section
26 Versions
Compare versions
43
files changed
+1962
additions
-516
deletions
| @@ -6,9 +6,18 @@ Website: https://hologram.page | |
| 6 6 | |
| 7 7 | ## Sponsors |
| 8 8 | |
| 9 | + ### Framework Visionary Tier |
| 10 | + |
| 11 | + * [@absowoot](https://github.com/absowoot) |
| 12 | + |
| 13 | + ### Early Adopter Tier |
| 14 | + |
| 9 15 | * Calancea Daniel, [@D4no0](https://github.com/D4no0) |
| 16 | + * Darren Black, [@totaltrash](https://github.com/totaltrash) |
| 10 17 | * Lucas Sifoni, [@Lucassifoni](https://github.com/Lucassifoni) |
| 11 18 | |
| 19 | + Thank you to all other sponsors for supporting the project! |
| 20 | + |
| 12 21 | If you find Hologram useful and would like to support its development, consider becoming a sponsor! Your contributions help improve the project and keep it alive. |
| 13 22 | |
| 14 23 | [Become a Sponsor](https://github.com/sponsors/bartblast) |
| @@ -32,6 +32,126 @@ export default class Bitstring { | |
| 32 32 | return $.#encoder.encode(text).length; |
| 33 33 | } |
| 34 34 | |
| 35 | + // Performs structural comparison of two bitstrings consistent with Elixir's behavior. |
| 36 | + // See: https://hexdocs.pm/elixir/main/Kernel.html#module-structural-comparison |
| 37 | + // "Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit." |
| 38 | + // Returns -1 if bitstring1 < bitstring2, 0 if equal, 1 if bitstring1 > bitstring2 |
| 39 | + static compare(bitstring1, bitstring2) { |
| 40 | + // Fast path: text-based comparison for identical text representations |
| 41 | + if (bitstring1.text !== null && bitstring2.text !== null) { |
| 42 | + if ( |
| 43 | + bitstring1.text === bitstring2.text && |
| 44 | + bitstring1.leftoverBitCount === bitstring2.leftoverBitCount |
| 45 | + ) { |
| 46 | + return 0; |
| 47 | + } |
| 48 | + // If both are text-based but different, fall through to byte comparison |
| 49 | + } |
| 50 | + |
| 51 | + // Ensure both bitstrings have bytes representation |
| 52 | + $.maybeSetBytesFromText(bitstring1); |
| 53 | + $.maybeSetBytesFromText(bitstring2); |
| 54 | + |
| 55 | + const bytes1 = bitstring1.bytes; |
| 56 | + const bytes2 = bitstring2.bytes; |
| 57 | + const leftover1 = bitstring1.leftoverBitCount; |
| 58 | + const leftover2 = bitstring2.leftoverBitCount; |
| 59 | + |
| 60 | + // Fast path: if both have no leftover bits, use optimized byte array comparison |
| 61 | + if (leftover1 === 0 && leftover2 === 0) { |
| 62 | + const len1 = bytes1.length; |
| 63 | + const len2 = bytes2.length; |
| 64 | + const minLen = Math.min(len1, len2); |
| 65 | + |
| 66 | + // Compare bytes in chunks for better performance |
| 67 | + let i = 0; |
| 68 | + |
| 69 | + // Process 4 bytes at a time when possible (unrolled loop) |
| 70 | + const end4 = minLen - (minLen % 4); |
| 71 | + for (; i < end4; i += 4) { |
| 72 | + if (bytes1[i] !== bytes2[i]) return bytes1[i] < bytes2[i] ? -1 : 1; |
| 73 | + if (bytes1[i + 1] !== bytes2[i + 1]) |
| 74 | + return bytes1[i + 1] < bytes2[i + 1] ? -1 : 1; |
| 75 | + if (bytes1[i + 2] !== bytes2[i + 2]) |
| 76 | + return bytes1[i + 2] < bytes2[i + 2] ? -1 : 1; |
| 77 | + if (bytes1[i + 3] !== bytes2[i + 3]) |
| 78 | + return bytes1[i + 3] < bytes2[i + 3] ? -1 : 1; |
| 79 | + } |
| 80 | + |
| 81 | + // Handle remaining bytes |
| 82 | + for (; i < minLen; i++) { |
| 83 | + if (bytes1[i] !== bytes2[i]) { |
| 84 | + return bytes1[i] < bytes2[i] ? -1 : 1; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + // All common bytes are equal, compare lengths |
| 89 | + return len1 === len2 ? 0 : len1 < len2 ? -1 : 1; |
| 90 | + } |
| 91 | + |
| 92 | + // Complex case: handle leftover bits |
| 93 | + const completeBytes1 = leftover1 === 0 ? bytes1.length : bytes1.length - 1; |
| 94 | + const completeBytes2 = leftover2 === 0 ? bytes2.length : bytes2.length - 1; |
| 95 | + const minCompleteBytes = Math.min(completeBytes1, completeBytes2); |
| 96 | + |
| 97 | + // Compare complete bytes first |
| 98 | + for (let i = 0; i < minCompleteBytes; i++) { |
| 99 | + if (bytes1[i] !== bytes2[i]) { |
| 100 | + return bytes1[i] < bytes2[i] ? -1 : 1; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Handle case where one bitstring has more complete bytes |
| 105 | + if (completeBytes1 !== completeBytes2) { |
| 106 | + const isFirstLonger = completeBytes1 > completeBytes2; |
| 107 | + const longerBytes = isFirstLonger ? bytes1 : bytes2; |
| 108 | + const shorterLeftover = isFirstLonger ? leftover2 : leftover1; |
| 109 | + const nextByte = longerBytes[minCompleteBytes]; |
| 110 | + |
| 111 | + if (shorterLeftover === 0) { |
| 112 | + // Shorter bitstring has no more bits |
| 113 | + return isFirstLonger ? 1 : -1; |
| 114 | + } |
| 115 | + |
| 116 | + // Compare next byte from longer with leftover bits from shorter |
| 117 | + const shorterBytes = isFirstLonger ? bytes2 : bytes1; |
| 118 | + const shorterLastByte = shorterBytes[shorterBytes.length - 1]; |
| 119 | + const mask = 0xff << (8 - shorterLeftover); |
| 120 | + const shorterMasked = shorterLastByte & mask; |
| 121 | + |
| 122 | + if (nextByte !== shorterMasked) { |
| 123 | + const result = nextByte < shorterMasked ? -1 : 1; |
| 124 | + return isFirstLonger ? result : -result; |
| 125 | + } |
| 126 | + |
| 127 | + // Equal up to leftover bits, longer one wins |
| 128 | + return isFirstLonger ? 1 : -1; |
| 129 | + } |
| 130 | + |
| 131 | + // Both have same number of complete bytes, compare leftover bits |
| 132 | + if (leftover1 === 0) { |
| 133 | + return leftover2 === 0 ? 0 : -1; |
| 134 | + } |
| 135 | + if (leftover2 === 0) { |
| 136 | + return 1; |
| 137 | + } |
| 138 | + |
| 139 | + // Both have leftover bits |
| 140 | + const lastByte1 = bytes1[bytes1.length - 1]; |
| 141 | + const lastByte2 = bytes2[bytes2.length - 1]; |
| 142 | + const minLeftover = Math.min(leftover1, leftover2); |
| 143 | + const mask = 0xff << (8 - minLeftover); |
| 144 | + const masked1 = lastByte1 & mask; |
| 145 | + const masked2 = lastByte2 & mask; |
| 146 | + |
| 147 | + if (masked1 !== masked2) { |
| 148 | + return masked1 < masked2 ? -1 : 1; |
| 149 | + } |
| 150 | + |
| 151 | + // Common bits equal, more leftover bits wins |
| 152 | + return leftover1 === leftover2 ? 0 : leftover1 < leftover2 ? -1 : 1; |
| 153 | + } |
| 154 | + |
| 35 155 | static concat(bitstrings) { |
| 36 156 | // Fast path: if a single bitstring is given return it as is |
| 37 157 | if (bitstrings.length === 1) { |
| @@ -128,6 +128,7 @@ export default class Client { | |
| 128 128 | method: "POST", |
| 129 129 | headers: { |
| 130 130 | "Content-Type": "application/json", |
| 131 | + "X-Csrf-Token": globalThis.hologram.csrfToken, |
| 131 132 | }, |
| 132 133 | body: Serializer.serialize($.buildCommandPayload(command), "server"), |
| 133 134 | }; |
| @@ -148,7 +149,7 @@ export default class Client { | |
| 148 149 | const nextAction = Interpreter.evaluateJavaScriptExpression(result); |
| 149 150 | |
| 150 151 | if (!Type.isNil(nextAction)) { |
| 151 | - Hologram.executeAction(nextAction); |
| 152 | + Hologram.scheduleAction(nextAction); |
| 152 153 | } |
| 153 154 | } catch (error) { |
| 154 155 | if (error instanceof HologramRuntimeError) { |
| @@ -1,123 +0,0 @@ | |
| 1 | - "use strict"; |
| 2 | - |
| 3 | - import Client from "./client.mjs"; |
| 4 | - import ComponentRegistry from "./component_registry.mjs"; |
| 5 | - import Hologram from "./hologram.mjs"; |
| 6 | - import HologramRuntimeError from "./errors/runtime_error.mjs"; |
| 7 | - import Interpreter from "./interpreter.mjs"; |
| 8 | - import Type from "./type.mjs"; |
| 9 | - |
| 10 | - export default class CommandQueue { |
| 11 | - // Made public to make tests easier |
| 12 | - static isProcessing = false; |
| 13 | - |
| 14 | - // Made public to make tests easier |
| 15 | - static items = {}; |
| 16 | - |
| 17 | - static fail(id) { |
| 18 | - CommandQueue.items[id].status = "failed"; |
| 19 | - ++CommandQueue.items[id].failCount; |
| 20 | - } |
| 21 | - |
| 22 | - static failAndThrowError(id, message) { |
| 23 | - CommandQueue.fail(id); |
| 24 | - throw new HologramRuntimeError(`command failed: ${message}`); |
| 25 | - } |
| 26 | - |
| 27 | - // Made public to make tests easier |
| 28 | - static getNextPending() { |
| 29 | - // The traversal order for string keys is ascending chronological (so we get FIFO behaviour) |
| 30 | - for (const id in CommandQueue.items) { |
| 31 | - if (CommandQueue.items[id].status === "pending") { |
| 32 | - return CommandQueue.items[id]; |
| 33 | - } |
| 34 | - } |
| 35 | - |
| 36 | - return null; |
| 37 | - } |
| 38 | - |
| 39 | - static process() { |
| 40 | - if (!CommandQueue.isProcessing && Client.isConnected()) { |
| 41 | - CommandQueue.isProcessing = true; |
| 42 | - |
| 43 | - let item; |
| 44 | - |
| 45 | - while ((item = CommandQueue.getNextPending())) { |
| 46 | - item.status = "sending"; |
| 47 | - |
| 48 | - const successCallback = ((currentItem) => { |
| 49 | - return (responsePayload) => { |
| 50 | - const [status, result] = responsePayload; |
| 51 | - |
| 52 | - if (status === 1) { |
| 53 | - CommandQueue.remove(currentItem.id); |
| 54 | - |
| 55 | - const nextAction = |
| 56 | - Interpreter.evaluateJavaScriptExpression(result); |
| 57 | - |
| 58 | - if (!Type.isNil(nextAction)) { |
| 59 | - Hologram.executeAction(nextAction); |
| 60 | - } |
| 61 | - } else { |
| 62 | - $.failAndThrowError(currentItem.id, result); |
| 63 | - } |
| 64 | - }; |
| 65 | - })(item); |
| 66 | - |
| 67 | - const failureCallback = ((currentItem) => { |
| 68 | - return (responsePayload) => { |
| 69 | - $.failAndThrowError(currentItem.id, responsePayload); |
| 70 | - }; |
| 71 | - })(item); |
| 72 | - |
| 73 | - const payload = CommandQueue.#buildPayload(item); |
| 74 | - |
| 75 | - Client.sendCommand(payload, successCallback, failureCallback); |
| 76 | - } |
| 77 | - |
| 78 | - CommandQueue.isProcessing = false; |
| 79 | - } |
| 80 | - } |
| 81 | - |
| 82 | - // Deps: [:maps.get/2] |
| 83 | - static push(command) { |
| 84 | - const target = Erlang_Maps["get/2"](Type.atom("target"), command); |
| 85 | - |
| 86 | - if (!ComponentRegistry.isCidRegistered(target)) { |
| 87 | - const msg = `invalid command target, there is no component with CID: ${Interpreter.inspect(target)}`; |
| 88 | - throw new HologramRuntimeError(msg); |
| 89 | - } |
| 90 | - |
| 91 | - const id = crypto.randomUUID(); |
| 92 | - const module = ComponentRegistry.getComponentModule(target); |
| 93 | - |
| 94 | - CommandQueue.items[id] = { |
| 95 | - id: id, |
| 96 | - failCount: 0, |
| 97 | - module: module, |
| 98 | - name: Erlang_Maps["get/2"](Type.atom("name"), command), |
| 99 | - params: Erlang_Maps["get/2"](Type.atom("params"), command), |
| 100 | - status: "pending", |
| 101 | - target: target, |
| 102 | - }; |
| 103 | - } |
| 104 | - |
| 105 | - static remove(id) { |
| 106 | - delete CommandQueue.items[id]; |
| 107 | - } |
| 108 | - |
| 109 | - static size() { |
| 110 | - return Object.keys(CommandQueue.items).length; |
| 111 | - } |
| 112 | - |
| 113 | - static #buildPayload(item) { |
| 114 | - return Type.map([ |
| 115 | - [Type.atom("module"), item.module], |
| 116 | - [Type.atom("name"), item.name], |
| 117 | - [Type.atom("params"), item.params], |
| 118 | - [Type.atom("target"), item.target], |
| 119 | - ]); |
| 120 | - } |
| 121 | - } |
| 122 | - |
| 123 | - const $ = CommandQueue; |
| @@ -236,6 +236,22 @@ const Erlang = { | |
| 236 236 | // End >=/2 |
| 237 237 | // Deps: [:erlang.==/2, :erlang.>/2] |
| 238 238 | |
| 239 | + // Start abs/1 |
| 240 | + "abs/1": (number) => { |
| 241 | + if (Type.isFloat(number)) { |
| 242 | + return Type.float(Math.abs(number.value)); |
| 243 | + } else if (Type.isInteger(number)) { |
| 244 | + const value = number.value; |
| 245 | + return Type.integer(value < 0n ? -value : value); |
| 246 | + } |
| 247 | + |
| 248 | + Interpreter.raiseArgumentError( |
| 249 | + Interpreter.buildArgumentErrorMsg(1, "not a number"), |
| 250 | + ); |
| 251 | + }, |
| 252 | + // End abs/1 |
| 253 | + // Deps: [] |
| 254 | + |
| 239 255 | // Start andalso/2 |
| 240 256 | "andalso/2": (leftFun, rightFun, context) => { |
| 241 257 | const left = leftFun(context); |
| @@ -343,6 +359,30 @@ const Erlang = { | |
| 343 359 | // End binary_to_existing_atom/2 |
| 344 360 | // Deps: [:erlang.binary_to_atom/2] |
| 345 361 | |
| 362 | + // Start binary_to_integer/1 |
| 363 | + "binary_to_integer/1": (binary) => { |
| 364 | + if (!Type.isBinary(binary)) { |
| 365 | + Interpreter.raiseArgumentError( |
| 366 | + Interpreter.buildArgumentErrorMsg(1, "not a binary"), |
| 367 | + ); |
| 368 | + } |
| 369 | + |
| 370 | + const text = Bitstring.toText(binary); |
| 371 | + |
| 372 | + if (!/^[+-]?\d+$/.test(text)) { |
| 373 | + Interpreter.raiseArgumentError( |
| 374 | + Interpreter.buildArgumentErrorMsg( |
| 375 | + 1, |
| 376 | + "not a textual representation of an integer", |
| 377 | + ), |
| 378 | + ); |
| 379 | + } |
| 380 | + |
| 381 | + return Type.integer(BigInt(text)); |
| 382 | + }, |
| 383 | + // End binary_to_integer/1 |
| 384 | + // Deps: [] |
| 385 | + |
| 346 386 | // Start bit_size/1 |
| 347 387 | "bit_size/1": (term) => { |
| 348 388 | if (!Type.isBitstring(term)) { |
| @@ -371,6 +411,32 @@ const Erlang = { | |
| 371 411 | // End byte_size/1 |
| 372 412 | // Deps: [] |
| 373 413 | |
| 414 | + // Start div/2 |
| 415 | + "div/2": (integer1, integer2) => { |
| 416 | + if (!Type.isInteger(integer1) || !Type.isInteger(integer2)) { |
| 417 | + const arg1 = Interpreter.inspect(integer1); |
| 418 | + const arg2 = Interpreter.inspect(integer2); |
| 419 | + |
| 420 | + Interpreter.raiseArgumentError( |
| 421 | + `bad argument in arithmetic expression: div(${arg1}, ${arg2})`, |
| 422 | + ); |
| 423 | + } |
| 424 | + |
| 425 | + if (integer2.value === 0n) { |
| 426 | + const arg1 = Interpreter.inspect(integer1); |
| 427 | + const arg2 = Interpreter.inspect(integer2); |
| 428 | + |
| 429 | + Interpreter.raiseArithmeticError(`div(${arg1}, ${arg2})`); |
| 430 | + } |
| 431 | + |
| 432 | + // TODO: support integers outside Number range |
| 433 | + return Type.integer( |
| 434 | + Math.trunc(Number(integer1.value) / Number(integer2.value)), |
| 435 | + ); |
| 436 | + }, |
| 437 | + // End div/2 |
| 438 | + // Deps: [] |
| 439 | + |
| 374 440 | // Start element/2 |
| 375 441 | "element/2": (index, tuple) => { |
| 376 442 | if (!Type.isInteger(index)) { |
Loading more files…