Current section

26 Versions

Jump to

Compare versions

6 files changed
+125 additions
-20 deletions
  @@ -23,7 +23,10 @@ export default class Bitstring {
23 23
24 24 static calculateSegmentBitCount(segment) {
25 25 const size = $.resolveSegmentSize(segment);
26 + if (size === null) return null;
27 +
26 28 const unit = $.resolveSegmentUnit(segment);
29 + if (unit === null) return null;
27 30
28 31 return size * unit;
29 32 }
  @@ -619,21 +622,11 @@ export default class Bitstring {
619 622 }
620 623
621 624 static resolveSegmentUnit(segment) {
622 - if (segment.unit !== null && segment.size !== null) {
625 + if (segment.unit !== null) {
623 626 return Number(segment.unit);
624 627 }
625 628
626 - switch (segment.type) {
627 - case "binary":
628 - return 8;
629 -
630 - case "float":
631 - case "integer":
632 - return 1;
633 -
634 - default:
635 - return null;
636 - }
629 + return segment.type === "binary" ? 8 : 1;
637 630 }
638 631
639 632 static serialize(bitstring) {
  @@ -361,15 +361,43 @@ const Erlang = {
361 361
362 362 // Start binary_to_integer/1
363 363 "binary_to_integer/1": (binary) => {
364 + return Erlang["binary_to_integer/2"](binary, Type.integer(10));
365 + },
366 + // End binary_to_integer/1
367 + // Deps: [:erlang.binary_to_integer/2]
368 +
369 + // Start binary_to_integer/2
370 + "binary_to_integer/2": (binary, base) => {
364 371 if (!Type.isBinary(binary)) {
365 372 Interpreter.raiseArgumentError(
366 373 Interpreter.buildArgumentErrorMsg(1, "not a binary"),
367 374 );
368 375 }
369 376
370 - const text = Bitstring.toText(binary);
377 + if (!Type.isInteger(base) || base.value < 2n || base.value > 36n) {
378 + Interpreter.raiseArgumentError(
379 + Interpreter.buildArgumentErrorMsg(
380 + 2,
381 + "not an integer in the range 2 through 36",
382 + ),
383 + );
384 + }
371 385
372 - if (!/^[+-]?\d+$/.test(text)) {
386 + const text = Bitstring.toText(binary);
387 + const baseNum = Number(base.value);
388 +
389 + let validPattern;
390 +
391 + // Validate the text representation based on the base
392 + if (baseNum <= 10) {
393 + const maxDigit = String(baseNum - 1);
394 + validPattern = new RegExp(`^[+-]?[0-${maxDigit}]+$`);
395 + } else {
396 + const maxLetter = String.fromCharCode(65 + baseNum - 11); // A=10, B=11, etc.
397 + validPattern = new RegExp(`^[+-]?[0-9A-${maxLetter}]+$`, "i");
398 + }
399 +
400 + if (!validPattern.test(text)) {
373 401 Interpreter.raiseArgumentError(
374 402 Interpreter.buildArgumentErrorMsg(
375 403 1,
  @@ -378,9 +406,14 @@ const Erlang = {
378 406 );
379 407 }
380 408
381 - return Type.integer(BigInt(text));
409 + // For base 10, use BigInt directly to avoid precision loss
410 + // For other bases, use parseInt which handles the base conversion
411 + const result =
412 + baseNum === 10 ? BigInt(text) : BigInt(parseInt(text, baseNum));
413 +
414 + return Type.integer(result);
382 415 },
383 - // End binary_to_integer/1
416 + // End binary_to_integer/2
384 417 // Deps: []
385 418
386 419 // Start bit_size/1
  @@ -828,6 +861,61 @@ const Erlang = {
828 861 // End orelse/2
829 862 // Deps: []
830 863
864 + // Start split_binary/2
865 + "split_binary/2": (binary, position) => {
866 + if (!Type.isBinary(binary)) {
867 + Interpreter.raiseArgumentError(
868 + Interpreter.buildArgumentErrorMsg(1, "not a binary"),
869 + );
870 + }
871 +
872 + if (!Type.isInteger(position)) {
873 + Interpreter.raiseArgumentError(
874 + Interpreter.buildArgumentErrorMsg(2, "not an integer"),
875 + );
876 + }
877 +
878 + if (position.value < 0n) {
879 + Interpreter.raiseArgumentError(
880 + Interpreter.buildArgumentErrorMsg(2, "out of range"),
881 + );
882 + }
883 +
884 + const pos = Number(position.value);
885 + const totalBytes = Number(Erlang["byte_size/1"](binary).value);
886 +
887 + if (pos > totalBytes) {
888 + Interpreter.raiseArgumentError(
889 + Interpreter.buildArgumentErrorMsg(2, "out of range"),
890 + );
891 + }
892 +
893 + // If position is 0, first part is empty binary
894 + if (pos === 0) {
895 + return Type.tuple([Type.bitstring(""), binary]);
896 + }
897 +
898 + // If position equals total size, second part is empty binary
899 + if (pos === totalBytes) {
900 + return Type.tuple([binary, Type.bitstring("")]);
901 + }
902 +
903 + // Split the binary using takeChunk
904 + // First part: from start to position (pos bytes)
905 + const firstPart = Bitstring.takeChunk(binary, 0, pos * 8);
906 +
907 + // Second part: from position to end
908 + const secondPart = Bitstring.takeChunk(
909 + binary,
910 + pos * 8,
911 + (totalBytes - pos) * 8,
912 + );
913 +
914 + return Type.tuple([firstPart, secondPart]);
915 + },
916 + // End split_binary/2
917 + // Deps: [:erlang.byte_size/1]
918 +
831 919 // Start tl/1
832 920 "tl/1": (list) => {
833 921 if (!Type.isList(list) || list.data.length === 0) {
  @@ -1313,8 +1313,10 @@ export default class Interpreter {
1313 1313 let chunkOffset = 0;
1314 1314 const rightBitCount = Bitstring.calculateBitCount(right);
1315 1315
1316 - for (const segment of left.segments) {
1316 + for (let i = 0; i < left.segments.length; i++) {
1317 + const segment = left.segments[i];
1317 1318 const segmentType = segment.type;
1319 + const isLastSegment = i === left.segments.length - 1;
1318 1320
1319 1321 if (
1320 1322 segmentType === "utf8" ||
  @@ -1327,7 +1329,23 @@ export default class Interpreter {
1327 1329 throw new HologramInterpreterError(message);
1328 1330 }
1329 1331
1330 - const chunkBitCount = Bitstring.calculateSegmentBitCount(segment);
1332 + let chunkBitCount;
1333 +
1334 + // Special case: last segment with binary or bitstring type and no explicit size
1335 + // should consume all remaining bits
1336 + if (
1337 + isLastSegment &&
1338 + (segmentType === "binary" || segmentType === "bitstring") &&
1339 + segment.size === null
1340 + ) {
1341 + chunkBitCount = rightBitCount - chunkOffset;
1342 + } else {
1343 + chunkBitCount = Bitstring.calculateSegmentBitCount(segment);
1344 +
1345 + if (chunkBitCount === null) {
1346 + return $.#handleMatchFail(right, raiseMatchError);
1347 + }
1348 + }
1331 1349
1332 1350 if (
1333 1351 segment.type === "float" &&
  @@ -1352,6 +1370,9 @@ export default class Interpreter {
1352 1370 context,
1353 1371 raiseMatchError,
1354 1372 );
1373 + } else if (segment.value.type === "match_placeholder") {
1374 + // Match placeholder in bitstring patterns just consumes the chunk without binding
1375 + // This is equivalent to _ in Elixir bitstring patterns
1355 1376 } else {
1356 1377 const segmentBitstring = Bitstring.fromSegments([segment]);
  @@ -7,7 +7,7 @@
7 7 {<<"Sponsor">>,<<"https://github.com/sponsors/bartblast">>},
8 8 {<<"Website">>,<<"https://hologram.page">>}]}.
9 9 {<<"name">>,<<"hologram">>}.
10 - {<<"version">>,<<"0.6.2">>}.
10 + {<<"version">>,<<"0.6.3">>}.
11 11 {<<"description">>,
12 12 <<"Full stack isomorphic Elixir web framework that can be used on top of Phoenix.">>}.
13 13 {<<"elixir">>,<<"~> 1.0">>}.
  @@ -16,6 +16,7 @@ defmodule Hologram.Compiler.CallGraph do
16 16 @type edge :: {vertex, vertex}
17 17 @type vertex :: module | mfa
18 18
19 + # TODO: Determine automatically based on deps annotations next to function implementations
19 20 @erlang_mfa_edges [
20 21 {{:erlang, :"=<", 2}, {:erlang, :<, 2}},
21 22 {{:erlang, :"=<", 2}, {:erlang, :==, 2}},
  @@ -25,9 +26,11 @@ defmodule Hologram.Compiler.CallGraph do
25 26 {{:erlang, :binary_to_atom, 1}, {:erlang, :binary_to_atom, 2}},
26 27 {{:erlang, :binary_to_existing_atom, 1}, {:erlang, :binary_to_atom, 1}},
27 28 {{:erlang, :binary_to_existing_atom, 2}, {:erlang, :binary_to_atom, 2}},
29 + {{:erlang, :binary_to_integer, 1}, {:erlang, :binary_to_integer, 2}},
28 30 {{:erlang, :error, 1}, {:erlang, :error, 2}},
29 31 {{:erlang, :integer_to_binary, 1}, {:erlang, :integer_to_binary, 2}},
30 32 {{:erlang, :iolist_to_binary, 1}, {:lists, :flatten, 1}},
33 + {{:erlang, :split_binary, 2}, {:erlang, :byte_size, 1}},
31 34 {{:lists, :keymember, 3}, {:lists, :keyfind, 3}},
32 35 {{:maps, :get, 2}, {:maps, :get, 3}},
33 36 {{:maps, :update, 3}, {:maps, :is_key, 2}},
Loading more files…