Current section

26 Versions

Jump to

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…