tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

source_text.zig (7990B)


      1 //! Unicode scalar boundary for source IO and the browser adapter. Binary/string ABI codecs remain byte-oriented.
      2 const std = @import("std");
      3 const Arena = @import("arena.zig").Arena;
      4 const codecs = @import("codecs.zig");
      5 const reduce = @import("reduce.zig");
      6 
      7 pub fn fromUtf8(arena: *Arena, bytes: []const u8) !u32 {
      8     const view = std.unicode.Utf8View.init(bytes) catch return error.InvalidUtf8;
      9     var iter = view.iterator();
     10     var scalars: std.ArrayList(u32) = .empty;
     11     defer scalars.deinit(arena.allocator);
     12     while (iter.nextCodepoint()) |scalar| {
     13         try scalars.append(arena.allocator, try codecs.ofNumber(arena, scalar));
     14     }
     15     return codecs.ofList(arena, scalars.items);
     16 }
     17 
     18 // Match the Haskell source boundary's canonical-tree check, not merely its
     19 // numeric interpretation. Reject redundant high zero bits, non-Boolean bits,
     20 // overlong integers, surrogates and values beyond the Unicode scalar range.
     21 fn scalarNumber(arena: *Arena, input: u32, unbounded: bool) !u21 {
     22     var current = input;
     23     var value: u32 = 0;
     24     var bits: u5 = 0;
     25     var high_bit: u32 = 0;
     26     while (true) {
     27         const node = arena.get(try observe(arena, current, unbounded)).*;
     28         switch (node) {
     29             .leaf => {
     30                 if (bits != 0 and high_bit == 0) return error.InvalidScalarList;
     31                 if (value > 0x10ffff or (value >= 0xd800 and value <= 0xdfff)) return error.InvalidScalarList;
     32                 return @intCast(value);
     33             },
     34             .fork => |pair| {
     35                 if (bits == 21) return error.InvalidScalarList;
     36                 const bit = arena.get(try observe(arena, pair.left, unbounded)).*;
     37                 high_bit = switch (bit) {
     38                     .leaf => 0,
     39                     .stem => |s| if (arena.get(try observe(arena, s.child, unbounded)).* == .leaf) 1 else return error.InvalidScalarList,
     40                     else => return error.InvalidScalarList,
     41                 };
     42                 value |= high_bit << bits;
     43                 bits += 1;
     44                 current = pair.right;
     45             },
     46             else => return error.InvalidScalarList,
     47         }
     48     }
     49 }
     50 
     51 pub fn toUtf8(arena: *Arena, input: u32) ![]u8 {
     52     return toUtf8Bounded(arena, input, std.math.maxInt(usize));
     53 }
     54 
     55 // A scalar-count ceiling, not a fuel/deadline limit. Do not demand elements
     56 // beyond the ceiling; still inspect the spine to distinguish exact length.
     57 fn observe(arena: *Arena, input: u32, unbounded: bool) !u32 {
     58     return if (unbounded) reduce.reduceUnbounded(input, arena) else reduce.reduce(input, arena, 1_000_000);
     59 }
     60 
     61 pub fn toUtf8UnboundedWork(arena: *Arena, input: u32, limit: usize) ![]u8 {
     62     return decode(arena, input, limit, true);
     63 }
     64 
     65 pub fn toUtf8Bounded(arena: *Arena, input: u32, limit: usize) ![]u8 {
     66     return decode(arena, input, limit, false);
     67 }
     68 
     69 fn decode(arena: *Arena, input: u32, limit: usize, unbounded: bool) ![]u8 {
     70     const base = arena.roots.items.len;
     71     defer arena.roots.shrinkRetainingCapacity(base);
     72     try arena.roots.append(arena.allocator, input);
     73     var bytes: std.ArrayList(u8) = .empty;
     74     errdefer bytes.deinit(arena.allocator);
     75     var current = input;
     76     var count: usize = 0;
     77     while (true) {
     78         const node = arena.get(try observe(arena, current, unbounded)).*;
     79         switch (node) {
     80             .leaf => return bytes.toOwnedSlice(arena.allocator),
     81             .fork => |pair| {
     82                 if (count == limit) return error.ScalarLimit;
     83                 count += 1;
     84                 const scalar = try scalarNumber(arena, pair.left, unbounded);
     85                 var encoded: [4]u8 = undefined;
     86                 const len = try std.unicode.utf8Encode(scalar, &encoded);
     87                 try bytes.appendSlice(arena.allocator, encoded[0..len]);
     88                 current = pair.right;
     89             },
     90             else => return error.InvalidScalarList,
     91         }
     92     }
     93 }
     94 
     95 test "source scalar codec preserves exact UTF-8 including NUL, CRLF and noncharacters" {
     96     var arena = Arena.init(std.testing.allocator);
     97     defer arena.deinit();
     98     const bytes = "\x00a\r\nĆ©šŸ˜€\u{ffff}\u{10ffff}\u{feff}";
     99     const scalars = try fromUtf8(&arena, bytes);
    100     var list = (try codecs.toList(&arena, scalars)).?;
    101     defer list.deinit(arena.allocator);
    102     const expected = [_]u64{ 0, 0x61, 13, 10, 0xe9, 0x1f600, 0xffff, 0x10ffff, 0xfeff };
    103     try std.testing.expectEqual(expected.len, list.items.len);
    104     for (list.items, expected) |item, n| try std.testing.expectEqual(n, (try codecs.toNumber(&arena, item)).?);
    105     arena.gc_enabled = true;
    106     try arena.setGcPolicy(1, 1);
    107     const result = try toUtf8(&arena, scalars);
    108     defer arena.allocator.free(result);
    109     try std.testing.expectEqualStrings(bytes, result);
    110     try std.testing.expect(arena.collections > 0);
    111     try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
    112 }
    113 
    114 test "source scalar codec rejects malformed UTF-8 and scalar trees" {
    115     var arena = Arena.init(std.testing.allocator);
    116     defer arena.deinit();
    117     for ([_][]const u8{ "\x80", "\xc0\xaf", "\xe2\x82", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xff" }) |bytes| {
    118         try std.testing.expectError(error.InvalidUtf8, fromUtf8(&arena, bytes));
    119     }
    120     for ([_]u64{ 0xd800, 0xdfff, 0x110000, std.math.maxInt(u64) }) |n| {
    121         const list = try codecs.ofList(&arena, &.{try codecs.ofNumber(&arena, n)});
    122         try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list));
    123     }
    124     const leaf = try arena.alloc(.leaf);
    125     const zero_with_bit = try arena.alloc(.{ .fork = .{ .left = leaf, .right = leaf } });
    126     const bad_bit = try arena.alloc(.{ .stem = .{ .child = zero_with_bit } });
    127     const malformed = try arena.alloc(.{ .fork = .{ .left = bad_bit, .right = leaf } });
    128     for ([_]u32{ zero_with_bit, malformed, bad_bit }) |n| {
    129         const list = try codecs.ofList(&arena, &.{n});
    130         try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list));
    131     }
    132     try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, bad_bit));
    133     // Failure after successful UTF-8 output must free the partial buffer too.
    134     const partial = try codecs.ofList(&arena, &.{ try codecs.ofNumber(&arena, 65), try codecs.ofNumber(&arena, 0xd800) });
    135     try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, partial));
    136 }
    137 
    138 test "source scalar codec accepts lazy scalar numerals" {
    139     var arena = Arena.init(std.testing.allocator);
    140     defer arena.deinit();
    141     const leaf = try arena.alloc(.leaf);
    142     const scalar = try codecs.ofNumber(&arena, 0x1f600);
    143     const constant = try arena.alloc(.{ .fork = .{ .left = leaf, .right = scalar } });
    144     const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = leaf } });
    145     const list = try codecs.ofList(&arena, &.{delayed});
    146     arena.gc_enabled = true;
    147     try arena.setGcPolicy(1, 1);
    148     const bytes = try toUtf8(&arena, list);
    149     defer arena.allocator.free(bytes);
    150     try std.testing.expectEqualStrings("šŸ˜€", bytes);
    151 }
    152 
    153 fn allocationProbe(allocator: std.mem.Allocator) !void {
    154     var arena = Arena.init(allocator);
    155     defer arena.deinit();
    156     const scalars = try fromUtf8(&arena, "aĆ©šŸ˜€");
    157     const bytes = try toUtf8(&arena, scalars);
    158     defer allocator.free(bytes);
    159     try std.testing.expectEqualStrings("aĆ©šŸ˜€", bytes);
    160 }
    161 
    162 test "source scalar codec cleans up on every allocation failure" {
    163     try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationProbe, .{});
    164 }
    165 
    166 
    167 test "bounded scalar encoding handles exact limits and unwinds partial output" {
    168     var arena = Arena.init(std.testing.allocator);
    169     defer arena.deinit();
    170     const input = try fromUtf8(&arena, "Ć©šŸ˜€");
    171     const bytes = try toUtf8Bounded(&arena, input, 2);
    172     defer arena.allocator.free(bytes);
    173     try std.testing.expectEqualStrings("Ć©šŸ˜€", bytes);
    174     try std.testing.expectError(error.ScalarLimit, toUtf8Bounded(&arena, input, 1));
    175     try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
    176 }