Skip to content

Commit 36f2603

Browse files
committed
Merge remote-tracking branch 'origin/main' into default-app-extra-targets
# Conflicts: # src/postcheck/monotype_lifted/spec_constr.zig
2 parents 2e5e50e + 19fd252 commit 36f2603

166 files changed

Lines changed: 6876 additions & 3025 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

design.md

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,8 @@ concrete monomorphic dispatcher type has already determined the owner.
872872

873873
A numeric literal whose target type is a non-builtin nominal type converts
874874
through that type's `from_numeral` method, and a string literal converts
875-
through `from_quote` (receiving the literal's post-escape UTF-8 bytes as
876-
`List(U8)`). Every such conversion with a concrete target type is a
875+
through `from_quote` (receiving the literal's post-escape contents as `Str`).
876+
Every such conversion with a concrete target type is a
877877
compile-time root (`numeral_conversion` / `quote_conversion`), no matter
878878
where the literal sits in the AST: checking finalization evaluates the raw
879879
dispatch call, stores its `Try` result through `ConstStore`, unwraps `Ok` into
@@ -901,20 +901,53 @@ encoding.
901901

902902
### String Interpolation
903903

904-
An interpolated string literal is canonicalization sugar. It desugars into
905-
ordinary CIR: the interpolated expressions bind to locals in source order,
906-
each literal segment stays a real string literal (so each converts through
907-
`from_quote`), and the result is
908-
`seg0.from_interpolation([].iter().prepended((interp_n, seg_n+1))...)` — the
909-
iterator yields each interpolated value paired with the literal segment that
910-
follows it. `from_interpolation : val, Iter((interpolated, val)) -> val` is an
911-
ordinary method: each implementing type chooses its `interpolated` type (`Str`
912-
chooses `Str`; a `Url`-style type can interpolate `Str` rather than itself),
913-
and a type that needs to validate assembled values simply does not implement
914-
it. The synthesized call node is recorded so checking unifies the call result
915-
with the receiver, pinning the literal's target type from the use site before
916-
string defaulting runs. No post-canonicalization stage knows interpolation
917-
exists.
904+
An interpolated string literal is its own CIR expression. It is not
905+
desugared as receiver method-call syntax, because interpolation method
906+
selection is owned by the expression result type, not by the first literal
907+
segment. The interpolated expressions bind to locals in source order. Literal
908+
segments are always builtin `Str` values, and the interpolation expression
909+
passes the first segment plus an `Iter((interpolated, Str))` of the remaining
910+
interpolated values paired with the literal segment that follows each one.
911+
912+
For an unsuffixed interpolation, checking gives the expression this type:
913+
914+
```roc
915+
val where [
916+
val.from_interpolation : Str, Iter((_interpolated, Str)) -> val,
917+
]
918+
```
919+
920+
The static dispatch owner is `val`, the interpolation result type. If `val`
921+
remains unconstrained, it defaults to `Str`, which selects:
922+
923+
```roc
924+
Str.from_interpolation : Str, Iter((Str, Str)) -> Str
925+
```
926+
927+
Types that want checked interpolation through `Try` implement their own
928+
`from_interpolation` and rely on `Try` forwarding:
929+
930+
```roc
931+
Try.from_interpolation : Str, Iter((interpolated, Str)) -> Try(ok, err)
932+
where [
933+
ok.from_interpolation : Str, Iter((interpolated, Str)) -> Try(ok, err),
934+
]
935+
```
936+
937+
For a suffixed interpolation such as `"a${x}b".Regex`, the suffix is not a
938+
static-dispatch owner. It is a direct associated-function call to
939+
`Regex.from_interpolation`; the function's argument types constrain the
940+
literal segments and interpolated expressions, and the function's return type is
941+
the type of the whole interpolation expression. Missing suffixed interpolation
942+
functions are reported as missing associated functions on the suffix type.
943+
944+
Interpolation deliberately does not parameterize literal segments over an
945+
arbitrary `literal` type with a `literal.from_quote` constraint. That design
946+
would defer quoted-segment conversion errors until monomorphic specializations
947+
are known. `roc check` must report all compile-time conversion errors without
948+
monomorphizing the program, so interpolation segments use builtin `Str`
949+
directly. Normal non-interpolated quoted literals still convert through
950+
`from_quote` as described above.
918951

919952
## Shared Post-Check Model
920953

src/base/CommonEnv.zig

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,29 @@ pub fn addExposedById(self: *CommonEnv, gpa: std.mem.Allocator, ident_idx: Ident
197197
return try self.exposed_items.addExposedById(gpa, @bitCast(ident_idx));
198198
}
199199

200-
/// Retrieves the node index associated with an exposed identifier.
201-
pub fn getNodeIndexById(self: *const CommonEnv, allocator: std.mem.Allocator, ident_idx: Ident.Idx) ?u32 {
202-
return self.exposed_items.getNodeIndexById(allocator, @bitCast(ident_idx));
200+
/// Retrieves the explicit target associated with an exposed identifier.
201+
pub fn getExposedTargetById(self: *const CommonEnv, allocator: std.mem.Allocator, ident_idx: Ident.Idx) ?collections.ExposedItemTarget {
202+
return self.exposed_items.getTargetById(allocator, @bitCast(ident_idx));
203203
}
204204

205-
/// Associates a node index with an exposed identifier.
206-
pub fn setNodeIndexById(self: *CommonEnv, gpa: std.mem.Allocator, ident_idx: Ident.Idx, node_idx: u32) Allocator.Error!void {
207-
return try self.exposed_items.setNodeIndexById(gpa, @bitCast(ident_idx), node_idx);
205+
/// Retrieves the value definition node associated with an exposed identifier.
206+
pub fn getValueNodeIndexById(self: *const CommonEnv, allocator: std.mem.Allocator, ident_idx: Ident.Idx) ?u32 {
207+
return self.exposed_items.getValueNodeIndexById(allocator, @bitCast(ident_idx));
208+
}
209+
210+
/// Retrieves the type declaration node associated with an exposed identifier.
211+
pub fn getTypeNodeIndexById(self: *const CommonEnv, allocator: std.mem.Allocator, ident_idx: Ident.Idx) ?u32 {
212+
return self.exposed_items.getTypeNodeIndexById(allocator, @bitCast(ident_idx));
213+
}
214+
215+
/// Associates a value definition node index with an exposed identifier.
216+
pub fn setValueNodeIndexById(self: *CommonEnv, gpa: std.mem.Allocator, ident_idx: Ident.Idx, node_idx: u32) Allocator.Error!void {
217+
return try self.exposed_items.setValueNodeIndexById(gpa, @bitCast(ident_idx), node_idx);
218+
}
219+
220+
/// Associates a type declaration node index with an exposed identifier.
221+
pub fn setTypeNodeIndexById(self: *CommonEnv, gpa: std.mem.Allocator, ident_idx: Ident.Idx, node_idx: u32) Allocator.Error!void {
222+
return try self.exposed_items.setTypeNodeIndexById(gpa, @bitCast(ident_idx), node_idx);
208223
}
209224

210225
/// Get region info for a given region

src/base/url.zig

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,151 @@
22

33
const std = @import("std");
44

5+
/// Compact slice coordinates for a package URL id inside a full URL.
6+
pub const UrlId = struct {
7+
start: u32,
8+
len: u32,
9+
10+
pub fn slice(self: UrlId, url: []const u8) []const u8 {
11+
const start: usize = self.start;
12+
return url[start..][0..self.len];
13+
}
14+
};
15+
16+
/// A package version parsed from a package URL path segment.
17+
///
18+
/// 0.0.0 is reserved as the "no version" sentinel and is rejected by URL
19+
/// parsing; the lowest publishable version is 0.0.1.
20+
pub const Version = struct {
21+
major: u32,
22+
minor: u32,
23+
patch: u32,
24+
25+
pub const none: Version = .{
26+
.major = 0,
27+
.minor = 0,
28+
.patch = 0,
29+
};
30+
31+
pub fn isPresent(self: Version) bool {
32+
return self.major != 0 or self.minor != 0 or self.patch != 0;
33+
}
34+
35+
/// Order two versions within the same major version: by minor, then patch.
36+
/// Asserts that the major versions match, since different major versions
37+
/// are different packages for solving purposes and must never be compared.
38+
pub fn orderWithinMajor(self: Version, other: Version) std.math.Order {
39+
std.debug.assert(self.major == other.major);
40+
const minor_order = std.math.order(self.minor, other.minor);
41+
if (minor_order != .eq) return minor_order;
42+
return std.math.order(self.patch, other.patch);
43+
}
44+
45+
pub fn eql(self: Version, other: Version) bool {
46+
return self.major == other.major and self.minor == other.minor and self.patch == other.patch;
47+
}
48+
};
49+
50+
/// The components parsed out of a package URL: the trailing content hash, the
51+
/// optional version path segment, and the span of the package's url id (the
52+
/// part between the scheme and the version/hash segments).
53+
pub const ParsedUrl = struct {
54+
hash: []const u8,
55+
version: Version,
56+
url_id: UrlId,
57+
58+
pub fn urlId(self: ParsedUrl, url: []const u8) []const u8 {
59+
return self.url_id.slice(url);
60+
}
61+
};
62+
63+
fn parseVersionPart(part: []const u8) ?u32 {
64+
if (part.len == 0) return null;
65+
66+
for (part) |char| {
67+
if (!std.ascii.isDigit(char)) return null;
68+
}
69+
70+
return std.fmt.parseInt(u32, part, 10) catch null;
71+
}
72+
73+
fn parseVersionComponent(component: []const u8) ?Version {
74+
var parts = std.mem.splitScalar(u8, component, '.');
75+
76+
const major_part = parts.next() orelse return null;
77+
const minor_part = parts.next() orelse return null;
78+
const patch_part = parts.next() orelse return null;
79+
if (parts.next() != null) return null;
80+
81+
return .{
82+
.major = parseVersionPart(major_part) orelse return null,
83+
.minor = parseVersionPart(minor_part) orelse return null,
84+
.patch = parseVersionPart(patch_part) orelse return null,
85+
};
86+
}
87+
88+
fn schemeContentStart(url: []const u8) ?usize {
89+
const scheme_marker = std.mem.find(u8, url, "://") orelse return null;
90+
return scheme_marker + 3;
91+
}
92+
93+
fn makeUrlId(url: []const u8, start: usize, end: usize) error{InvalidUrl}!UrlId {
94+
var trimmed_end = end;
95+
while (trimmed_end > start and url[trimmed_end - 1] == '/') {
96+
trimmed_end -= 1;
97+
}
98+
99+
if (trimmed_end <= start) return error.InvalidUrl;
100+
101+
return .{
102+
.start = std.math.cast(u32, start) orelse return error.InvalidUrl,
103+
.len = std.math.cast(u32, trimmed_end - start) orelse return error.InvalidUrl,
104+
};
105+
}
106+
107+
/// Parse a package URL's path into its trailing content hash, optional
108+
/// MAJOR.MINOR.PATCH version segment, and url id span.
109+
pub fn parseUrlPath(url: []const u8) error{ InvalidUrl, InvalidVersion, NoHashInUrl }!ParsedUrl {
110+
const url_id_start = schemeContentStart(url) orelse return error.InvalidUrl;
111+
const last_slash = std.mem.findLast(u8, url, "/") orelse return error.NoHashInUrl;
112+
if (last_slash < url_id_start) return error.NoHashInUrl;
113+
114+
const hash_part = url[last_slash + 1 ..];
115+
116+
const hash = if (std.mem.endsWith(u8, hash_part, ".tar.zst"))
117+
hash_part[0 .. hash_part.len - 8]
118+
else
119+
hash_part;
120+
121+
if (hash.len == 0) {
122+
return error.NoHashInUrl;
123+
}
124+
125+
const before_hash = url[0..last_slash];
126+
const version_parse = if (std.mem.findLast(u8, before_hash, "/")) |version_slash|
127+
if (version_slash >= url_id_start) parseVersionComponent(before_hash[version_slash + 1 ..]) else null
128+
else
129+
null;
130+
if (version_parse) |parsed_version| {
131+
// 0.0.0 is reserved as the no-version sentinel; the lowest publishable
132+
// version is 0.0.1.
133+
if (!parsed_version.isPresent()) return error.InvalidVersion;
134+
}
135+
const version = version_parse orelse Version.none;
136+
const url_id_end = if (version_parse != null)
137+
std.mem.findLast(u8, before_hash, "/").?
138+
else
139+
last_slash;
140+
141+
const url_id = makeUrlId(url, url_id_start, url_id_end) catch return error.InvalidUrl;
142+
143+
return .{
144+
.hash = hash,
145+
.version = version,
146+
.url_id = url_id,
147+
};
148+
}
149+
5150
/// Checks if a URL is safe. Used for platform specification.
6151
///
7152
/// Allows:
@@ -42,3 +187,77 @@ test "isSafeUrl" {
42187
try testing.expect(!isSafeUrl("/absolute/path"));
43188
try testing.expect(!isSafeUrl("platform.roc"));
44189
}
190+
191+
test "UrlId returns slice from full URL" {
192+
const url = "https://example.com/foo/bar/1.2.3/hash";
193+
const id = UrlId{ .start = 8, .len = 19 };
194+
195+
try std.testing.expectEqualStrings("example.com/foo/bar", id.slice(url));
196+
}
197+
198+
test "parseUrlPath extracts url id" {
199+
{
200+
const url = "https://example.com/foo/bar/1.2.3/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst";
201+
const parsed = try parseUrlPath(url);
202+
203+
try std.testing.expectEqualStrings("example.com/foo/bar", parsed.urlId(url));
204+
try std.testing.expectEqual(Version{ .major = 1, .minor = 2, .patch = 3 }, parsed.version);
205+
}
206+
207+
{
208+
const url = "https://example.com/foo/bar/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst";
209+
const parsed = try parseUrlPath(url);
210+
211+
try std.testing.expectEqualStrings("example.com/foo/bar", parsed.urlId(url));
212+
try std.testing.expectEqual(Version.none, parsed.version);
213+
}
214+
215+
{
216+
const url = "http://127.0.0.1:8000/1.2.3/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst";
217+
const parsed = try parseUrlPath(url);
218+
219+
try std.testing.expectEqualStrings("127.0.0.1:8000", parsed.urlId(url));
220+
}
221+
222+
{
223+
const url = "https://example.com/foo/1.2.x/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst";
224+
const parsed = try parseUrlPath(url);
225+
226+
try std.testing.expectEqualStrings("example.com/foo/1.2.x", parsed.urlId(url));
227+
}
228+
229+
{
230+
const url = "https://example.com/foo/0.0.1/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst";
231+
const parsed = try parseUrlPath(url);
232+
233+
try std.testing.expectEqualStrings("example.com/foo", parsed.urlId(url));
234+
try std.testing.expectEqual(Version{ .major = 0, .minor = 0, .patch = 1 }, parsed.version);
235+
}
236+
}
237+
238+
test "parseUrlPath rejects the reserved 0.0.0 version" {
239+
try std.testing.expectError(
240+
error.InvalidVersion,
241+
parseUrlPath("https://example.com/0.0.0/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"),
242+
);
243+
try std.testing.expectError(
244+
error.InvalidVersion,
245+
parseUrlPath("https://example.com/foo/bar/0.0.0/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"),
246+
);
247+
}
248+
249+
test "parseUrlPath rejects URLs without a hash path segment" {
250+
try std.testing.expectError(error.NoHashInUrl, parseUrlPath("https://example.com"));
251+
try std.testing.expectError(error.NoHashInUrl, parseUrlPath("https://example.com/"));
252+
}
253+
254+
test "Version.orderWithinMajor orders by minor then patch" {
255+
const v1_2_3 = Version{ .major = 1, .minor = 2, .patch = 3 };
256+
const v1_3_1 = Version{ .major = 1, .minor = 3, .patch = 1 };
257+
const v1_2_4 = Version{ .major = 1, .minor = 2, .patch = 4 };
258+
259+
try std.testing.expectEqual(std.math.Order.lt, v1_2_3.orderWithinMajor(v1_3_1));
260+
try std.testing.expectEqual(std.math.Order.lt, v1_2_3.orderWithinMajor(v1_2_4));
261+
try std.testing.expectEqual(std.math.Order.gt, v1_3_1.orderWithinMajor(v1_2_4));
262+
try std.testing.expectEqual(std.math.Order.eq, v1_2_3.orderWithinMajor(v1_2_3));
263+
}

0 commit comments

Comments
 (0)