Merge branch 'less-process-exit' of github.com:roc-lang/roc into less-process-exit

This commit is contained in:
Anton-4 2025-10-29 09:58:54 +01:00
commit 34d9bddec9
No known key found for this signature in database
29 changed files with 563 additions and 180 deletions

View file

@ -84,7 +84,7 @@ pub const Attributes = packed struct(u3) {
return .{
.effectful = std.mem.endsWith(u8, text, "!"),
.ignored = std.mem.startsWith(u8, text, "_"),
.reassignable = false,
.reassignable = std.mem.startsWith(u8, text, "$"),
};
}
};
@ -319,6 +319,14 @@ test "from_bytes creates ignored identifier" {
try std.testing.expect(result.attributes.reassignable == false);
}
test "from_bytes creates reassignable identifier" {
const result = try Ident.from_bytes("$reusable");
try std.testing.expectEqualStrings("$reusable", result.raw_text);
try std.testing.expect(result.attributes.effectful == false);
try std.testing.expect(result.attributes.ignored == false);
try std.testing.expect(result.attributes.reassignable == true);
}
test "Ident.Store empty CompactWriter roundtrip" {
const gpa = std.testing.allocator;

View file

@ -130,7 +130,7 @@ pub const CanonicalizedExpr = struct {
const TypeVarProblemKind = enum {
unused_type_var,
type_var_marked_unused,
type_var_ending_in_underscore,
type_var_starting_with_dollar,
};
const TypeVarProblem = struct {
@ -5320,9 +5320,9 @@ fn scopeIntroduceVar(
}
fn collectTypeVarProblems(ident: Ident.Idx, is_single_use: bool, ast_anno: AST.TypeAnno.Idx, scratch: *base.Scratch(TypeVarProblem)) std.mem.Allocator.Error!void {
// Warn for type variables with trailing underscores
// Warn for type variables starting with dollar sign (reusable markers)
if (ident.attributes.reassignable) {
try scratch.append(.{ .ident = ident, .problem = .type_var_ending_in_underscore, .ast_anno = ast_anno });
try scratch.append(.{ .ident = ident, .problem = .type_var_starting_with_dollar, .ast_anno = ast_anno });
}
// Should start with underscore but doesn't, or should not start with underscore but does.
@ -5338,11 +5338,11 @@ fn reportTypeVarProblems(self: *Self, problems: []const TypeVarProblem) std.mem.
const name_text = self.env.getIdent(problem.ident);
switch (problem.problem) {
.type_var_ending_in_underscore => {
const suggested_name_text = name_text[0 .. name_text.len - 1]; // Remove the trailing underscore
.type_var_starting_with_dollar => {
const suggested_name_text = name_text[1..]; // Remove the leading dollar sign
const suggested_ident = self.env.insertIdent(base.Ident.for_text(suggested_name_text), Region.zero());
self.env.pushDiagnostic(Diagnostic{ .type_var_ending_in_underscore = .{
self.env.pushDiagnostic(Diagnostic{ .type_var_starting_with_dollar = .{
.name = problem.ident,
.suggested_name = suggested_ident,
.region = region,
@ -5410,11 +5410,11 @@ fn processCollectedTypeVars(self: *Self) std.mem.Allocator.Error!void {
const name_text = self.env.getIdent(problem.ident);
switch (problem.problem) {
.type_var_ending_in_underscore => {
const suggested_name_text = name_text[0 .. name_text.len - 1]; // Remove the trailing underscore
.type_var_starting_with_dollar => {
const suggested_name_text = name_text[1..]; // Remove the leading dollar sign
const suggested_ident = self.env.insertIdent(base.Ident.for_text(suggested_name_text), Region.zero());
self.env.pushDiagnostic(Diagnostic{ .type_var_ending_in_underscore = .{
self.env.pushDiagnostic(Diagnostic{ .type_var_starting_with_dollar = .{
.name = problem.ident,
.suggested_name = suggested_ident,
.region = Region.zero(),
@ -6718,35 +6718,91 @@ pub fn canonicalizeBlockStatement(self: *Self, ast_stmt: AST.Statement, ast_stmt
},
else => {
// If the next stmt does not match this annotation,
// then just add the annotation independently
// create a Def with an e_anno_only body
// TODO: Capture diagnostic that this anno doesn't
// have a corresponding def
const stmt_idx = try self.env.addStatement(Statement{
.s_type_anno = .{
.name = name_ident,
.anno = type_anno_idx,
.where = where_clauses,
// Create the pattern for this def
const pattern = Pattern{
.assign = .{
.ident = name_ident,
},
}, region);
};
const pattern_idx = try self.env.addPattern(pattern, region);
// Introduce the name to scope
switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, pattern_idx, false, true)) {
.success => {},
.shadowing_warning => |shadowed_pattern_idx| {
const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx);
try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{
.ident = name_ident,
.region = region,
.original_region = original_region,
} });
},
else => {},
}
// Create the e_anno_only expression
const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region);
// Create the annotation structure
const annotation = CIR.Annotation{
.anno = type_anno_idx,
.where = where_clauses,
};
const annotation_idx = try self.env.addAnnotation(annotation, region);
// Add the decl as a statement with the e_anno_only body
const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{
.pattern = pattern_idx,
.expr = anno_only_expr,
.anno = annotation_idx,
} }, region);
mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = null };
},
}
} else {
// If the next stmt does not match this annotation,
// then just add the annotation independently
// create a Def with an e_anno_only body
// TODO: Capture diagnostic that this anno doesn't
// have a corresponding def
const stmt_idx = try self.env.addStatement(Statement{
.s_type_anno = .{
.name = name_ident,
.anno = type_anno_idx,
.where = where_clauses,
// Create the pattern for this def
const pattern = Pattern{
.assign = .{
.ident = name_ident,
},
}, region);
};
const pattern_idx = try self.env.addPattern(pattern, region);
// Introduce the name to scope
switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, pattern_idx, false, true)) {
.success => {},
.shadowing_warning => |shadowed_pattern_idx| {
const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx);
try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{
.ident = name_ident,
.region = region,
.original_region = original_region,
} });
},
else => {},
}
// Create the e_anno_only expression
const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region);
// Create the annotation structure
const annotation = CIR.Annotation{
.anno = type_anno_idx,
.where = where_clauses,
};
const annotation_idx = try self.env.addAnnotation(annotation, region);
// Add the decl as a statement with the e_anno_only body
const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{
.pattern = pattern_idx,
.expr = anno_only_expr,
.anno = annotation_idx,
} }, region);
mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = null };
}
},

View file

@ -239,7 +239,7 @@ fn collectExprDependencies(
},
// Literals have no dependencies
.e_num, .e_frac_f32, .e_frac_f64, .e_dec, .e_dec_small, .e_str, .e_str_segment, .e_empty_list, .e_empty_record, .e_zero_argument_tag, .e_ellipsis => {},
.e_num, .e_frac_f32, .e_frac_f64, .e_dec, .e_dec_small, .e_str, .e_str_segment, .e_empty_list, .e_empty_record, .e_zero_argument_tag, .e_ellipsis, .e_anno_only => {},
// External lookups reference other modules - skip for now
.e_lookup_external => {},

View file

@ -235,7 +235,7 @@ pub const Diagnostic = union(enum) {
suggested_name: Ident.Idx,
region: Region,
},
type_var_ending_in_underscore: struct {
type_var_starting_with_dollar: struct {
name: Ident.Idx,
suggested_name: Ident.Idx,
region: Region,
@ -302,7 +302,7 @@ pub const Diagnostic = union(enum) {
.f64_pattern_literal => |d| d.region,
.unused_type_var_name => |d| d.region,
.type_var_marked_unused => |d| d.region,
.type_var_ending_in_underscore => |d| d.region,
.type_var_starting_with_dollar => |d| d.region,
.underscore_in_type_declaration => |d| d.region,
};
}

View file

@ -357,6 +357,14 @@ pub const Expr = union(enum) {
/// launchTheNukes: |{}| ...
/// ```
e_ellipsis: struct {},
/// A standalone type annotation without a body.
/// This represents a type declaration that has no implementation.
/// During type-checking, this expression is assigned the type from its annotation.
///
/// ```roc
/// foo : {} -> {}
/// ```
e_anno_only: struct {},
pub const Idx = enum(u32) { _ };
pub const Span = struct { span: DataSpan };
@ -977,6 +985,14 @@ pub const Expr = union(enum) {
const attrs = tree.beginNode();
try tree.endNode(begin, attrs);
},
.e_anno_only => |_| {
const begin = tree.beginNode();
try tree.pushStaticAtom("e-anno-only");
const region = ir.store.getExprRegion(expr_idx);
try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
const attrs = tree.beginNode();
try tree.endNode(begin, attrs);
},
.e_crash => |e| {
const begin = tree.beginNode();
try tree.pushStaticAtom("e-crash");

View file

@ -77,6 +77,7 @@ pub const Tag = enum {
expr_crash,
expr_block,
expr_ellipsis,
expr_anno_only,
expr_expect,
expr_record_builder,
match_branch,
@ -201,7 +202,7 @@ pub const Tag = enum {
diag_f64_pattern_literal,
diag_unused_type_var_name,
diag_type_var_marked_unused,
diag_type_var_ending_in_underscore,
diag_type_var_starting_with_dollar,
diag_underscore_in_type_declaration,
diagnostic_exposed_but_not_implemented,
diag_redundant_exposed,

View file

@ -130,7 +130,7 @@ pub fn deinit(store: *NodeStore) void {
/// Count of the diagnostic nodes in the ModuleEnv
pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 57;
/// Count of the expression nodes in the ModuleEnv
pub const MODULEENV_EXPR_NODE_COUNT = 33;
pub const MODULEENV_EXPR_NODE_COUNT = 34;
/// Count of the statement nodes in the ModuleEnv
pub const MODULEENV_STATEMENT_NODE_COUNT = 14;
/// Count of the type annotation nodes in the ModuleEnv
@ -628,6 +628,9 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr {
.expr_ellipsis => {
return CIR.Expr{ .e_ellipsis = .{} };
},
.expr_anno_only => {
return CIR.Expr{ .e_anno_only = .{} };
},
.expr_expect => {
return CIR.Expr{ .e_expect = .{
.body = @enumFromInt(node.data_1),
@ -1479,6 +1482,9 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator
.e_ellipsis => |_| {
node.tag = .expr_ellipsis;
},
.e_anno_only => |_| {
node.tag = .expr_anno_only;
},
.e_match => |e| {
node.tag = .expr_match;
@ -2913,8 +2919,8 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) Allocator.Error!
node.data_1 = @bitCast(r.name);
node.data_2 = @bitCast(r.suggested_name);
},
.type_var_ending_in_underscore => |r| {
node.tag = .diag_type_var_ending_in_underscore;
.type_var_starting_with_dollar => |r| {
node.tag = .diag_type_var_starting_with_dollar;
region = r.region;
node.data_1 = @bitCast(r.name);
node.data_2 = @bitCast(r.suggested_name);
@ -3228,7 +3234,7 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI
.suggested_name = @bitCast(node.data_2),
.region = store.getRegionAt(node_idx),
} },
.diag_type_var_ending_in_underscore => return CIR.Diagnostic{ .type_var_ending_in_underscore = .{
.diag_type_var_starting_with_dollar => return CIR.Diagnostic{ .type_var_starting_with_dollar = .{
.name = @bitCast(node.data_1),
.suggested_name = @bitCast(node.data_2),
.region = store.getRegionAt(node_idx),

View file

@ -29,6 +29,7 @@ test "compile tests" {
std.testing.refAllDecls(@import("Statement.zig"));
std.testing.refAllDecls(@import("TypeAnnotation.zig"));
std.testing.refAllDecls(@import("test/anno_only_test.zig"));
std.testing.refAllDecls(@import("test/bool_test.zig"));
std.testing.refAllDecls(@import("test/exposed_shadowing_test.zig"));
std.testing.refAllDecls(@import("test/frac_test.zig"));

View file

@ -0,0 +1,43 @@
//! Tests for standalone type annotation canonicalization.
//!
//! This module contains unit tests that verify the e_anno_only expression variant
//! works correctly in the compiler's canonical internal representation (CIR).
const std = @import("std");
const testing = std.testing;
const CIR = @import("../CIR.zig");
test "e_anno_only expression variant exists" {
// Create an e_anno_only expression
const expr = CIR.Expr{ .e_anno_only = .{} };
// Verify it's the correct variant
switch (expr) {
.e_anno_only => {},
else => return error.WrongExprVariant,
}
}
test "e_anno_only can be used in statements" {
// This test verifies that e_anno_only expressions can be
// used as part of s_decl statements, which is how standalone
// type annotations are represented after canonicalization.
const pattern_idx: CIR.Pattern.Idx = @enumFromInt(0);
const expr_idx: CIR.Expr.Idx = @enumFromInt(0);
const anno_idx: CIR.Annotation.Idx = @enumFromInt(0);
const stmt = CIR.Statement{ .s_decl = .{
.pattern = pattern_idx,
.expr = expr_idx,
.anno = anno_idx,
} };
// Verify the statement was created correctly
switch (stmt) {
.s_decl => |decl| {
try testing.expect(decl.anno != null);
},
else => return error.WrongStatementType,
}
}

View file

@ -712,7 +712,7 @@ test "NodeStore round trip - Diagnostics" {
});
try diagnostics.append(gpa, CIR.Diagnostic{
.type_var_ending_in_underscore = .{
.type_var_starting_with_dollar = .{
.name = rand_ident_idx(),
.suggested_name = rand_ident_idx(),
.region = rand_region(),

View file

@ -2976,6 +2976,21 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected
.e_ellipsis => {
try self.updateVar(expr_var, .{ .flex = Flex.init() }, rank);
},
.e_anno_only => {
// For annotation-only expressions, the type comes from the annotation.
// This case should only occur when the expression has an annotation (which is
// enforced during canonicalization), so the expected type should be set.
// The type will be unified with the expected type in the code below.
switch (expected) {
.no_expectation => {
// This shouldn't happen since we always create e_anno_only with an annotation
try self.updateVar(expr_var, .err, rank);
},
.expected => {
// The expr_var will be unified with the annotation var below
},
}
},
.e_runtime_error => {
try self.updateVar(expr_var, .err, rank);
},

View file

@ -751,7 +751,33 @@ pub const BuildEnv = struct {
// Read source
const file_abs = try std.fs.path.resolve(self.gpa, &.{file_path});
defer self.gpa.free(file_abs);
const src = try std.fs.cwd().readFileAlloc(self.gpa, file_abs, std.math.maxInt(usize));
const src = std.fs.cwd().readFileAlloc(self.gpa, file_abs, std.math.maxInt(usize)) catch |err| {
const report = blk: switch (err) {
error.FileNotFound => {
var report = Report.init(self.gpa, "FILE NOT FOUND", .fatal);
try report.document.addText("I could not find the file ");
try report.document.addAnnotated(file_abs, .path);
try report.document.addLineBreak();
try report.document.addText("Make sure the file exists and you do not have any typos in its name or path.");
break :blk report;
},
else => {
var report = Report.init(self.gpa, "COULD NOT READ FILE", .fatal);
try report.document.addText("I could not read the file ");
try report.document.addAnnotated(file_abs, .path);
try report.document.addLineBreak();
try report.document.addText("I did get the following error: ");
try report.addErrorMessage(@errorName(err));
try report.document.addText("Make sure the file can be read.");
break :blk report;
},
};
self.sink.emitReport("main", file_abs, report);
try self.sink.buildOrder(&[_][]const u8{"main"}, &[_][]const u8{file_abs}, &[_]u32{0});
self.sink.tryEmit();
return err;
};
defer self.gpa.free(src);
var env = try ModuleEnv.init(self.gpa, src);

View file

@ -435,6 +435,7 @@ pub const ComptimeEvaluator = struct {
try self.interpreter.bindings.append(.{
.pattern_idx = def.pattern,
.value = value,
.expr_idx = def.expr,
});
}
},

View file

@ -98,7 +98,7 @@ pub const Interpreter = struct {
return std.mem.eql(types.Var, a.args_ptr[0..a.args_len], b.args_ptr[0..b.args_len]);
}
};
const Binding = struct { pattern_idx: can.CIR.Pattern.Idx, value: StackValue };
const Binding = struct { pattern_idx: can.CIR.Pattern.Idx, value: StackValue, expr_idx: can.CIR.Expr.Idx };
const DefInProgress = struct {
pattern_idx: can.CIR.Pattern.Idx,
expr_idx: can.CIR.Expr.Idx,
@ -316,7 +316,7 @@ pub const Interpreter = struct {
while (j < params.len) : (j += 1) {
const sorted_idx = args_accessor.findElementIndexByOriginal(j) orelse j;
const arg_value = try args_accessor.getElement(sorted_idx);
const matched = try self.patternMatchesBind(params[j], arg_value, param_rt_vars[j], roc_ops, &temp_binds);
const matched = try self.patternMatchesBind(params[j], arg_value, param_rt_vars[j], roc_ops, &temp_binds, @enumFromInt(0));
if (!matched) return error.TypeMismatch;
}
}
@ -402,7 +402,7 @@ pub const Interpreter = struct {
.source_env = self_interp.env,
};
}
try self_interp.bindings.append(.{ .pattern_idx = patt_idx, .value = ph });
try self_interp.bindings.append(.{ .pattern_idx = patt_idx, .value = ph, .expr_idx = rhs_expr });
}
};
switch (stmt) {
@ -442,7 +442,7 @@ pub const Interpreter = struct {
const val = try self.evalExprMinimal(d.expr, roc_ops, expr_rt_var);
defer val.decref(&self.runtime_layout_store, roc_ops);
if (!try self.patternMatchesBind(d.pattern, val, expr_rt_var, roc_ops, &temp_binds)) {
if (!try self.patternMatchesBind(d.pattern, val, expr_rt_var, roc_ops, &temp_binds, d.expr)) {
return error.TypeMismatch;
}
@ -463,7 +463,7 @@ pub const Interpreter = struct {
const val = try self.evalExprMinimal(v.expr, roc_ops, expr_rt_var);
defer val.decref(&self.runtime_layout_store, roc_ops);
if (!try self.patternMatchesBind(v.pattern_idx, val, expr_rt_var, roc_ops, &temp_binds)) {
if (!try self.patternMatchesBind(v.pattern_idx, val, expr_rt_var, roc_ops, &temp_binds, v.expr)) {
return error.TypeMismatch;
}
@ -558,7 +558,7 @@ pub const Interpreter = struct {
temp_binds.deinit();
}
if (!try self.patternMatchesBind(for_stmt.patt, elem_value, patt_rt_var, roc_ops, &temp_binds)) {
if (!try self.patternMatchesBind(for_stmt.patt, elem_value, patt_rt_var, roc_ops, &temp_binds, @enumFromInt(0))) {
elem_value.decref(&self.runtime_layout_store, roc_ops);
return error.TypeMismatch;
}
@ -1294,7 +1294,7 @@ pub const Interpreter = struct {
for (patterns) |bp_idx| {
self.trimBindingList(&temp_binds, 0, roc_ops);
if (!try self.patternMatchesBind(self.env.store.getMatchBranchPattern(bp_idx).pattern, scrutinee, scrutinee_rt_var, roc_ops, &temp_binds)) {
if (!try self.patternMatchesBind(self.env.store.getMatchBranchPattern(bp_idx).pattern, scrutinee, scrutinee_rt_var, roc_ops, &temp_binds, @enumFromInt(0))) {
self.trimBindingList(&temp_binds, 0, roc_ops);
continue;
}
@ -1376,6 +1376,38 @@ pub const Interpreter = struct {
}
return value;
},
.e_anno_only => |_| {
// This represents a value that only has a type annotation, no implementation
const ct_var = can.ModuleEnv.varFrom(expr_idx);
const rt_var = try self.translateTypeVar(self.env, ct_var);
const anno_layout = try self.getRuntimeLayout(rt_var);
if (anno_layout.tag == .closure) {
// Function type: Build a closure-like value that will crash when called
const value = try self.pushRaw(anno_layout, 0);
self.registerDefValue(expr_idx, value);
// Initialize the closure header with the e_anno_only expr itself as the body
// This serves as a marker that will be detected during call evaluation
if (value.ptr) |ptr| {
const header: *layout.Closure = @ptrCast(@alignCast(ptr));
header.* = .{
.body_idx = expr_idx, // Point to self (the e_anno_only expression)
.params = .{ .span = .{ .start = 0, .len = 0 } }, // No params
.captures_pattern_idx = @enumFromInt(@as(u32, 0)),
.captures_layout_idx = anno_layout.data.closure.captures_layout_idx,
.lambda_expr_idx = expr_idx,
.source_env = self.env,
};
}
return value;
} else {
// Non-function type: Create a value that will be marked as annotation-only
// We'll detect this during lookup and crash then
const value = try self.pushRaw(anno_layout, 0);
self.registerDefValue(expr_idx, value);
return value;
}
},
.e_closure => |cls| {
// Build a closure value with concrete captures. The closure references a lambda.
const lam_expr = self.env.store.getExpr(cls.lambda_idx);
@ -1544,6 +1576,13 @@ pub const Interpreter = struct {
self.bindings.shrinkRetainingCapacity(saved_bindings_len);
}
// Check if this is an annotation-only function (body points to e_anno_only)
const body_expr = self.env.store.getExpr(header.body_idx);
if (body_expr == .e_anno_only) {
self.triggerCrash("This function has no implementation. It is only a type annotation for now.", false, roc_ops);
return error.Crash;
}
const params = self.env.store.slicePatterns(header.params);
if (params.len != arg_indices.len) return error.TypeMismatch;
// Provide closure context for capture lookup during body eval
@ -1551,7 +1590,7 @@ pub const Interpreter = struct {
defer _ = self.active_closures.pop();
var bind_count: usize = 0;
while (bind_count < params.len) : (bind_count += 1) {
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = arg_values[bind_count] });
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = arg_values[bind_count], .expr_idx = @enumFromInt(0) });
}
defer {
var k = params.len;
@ -1573,7 +1612,7 @@ pub const Interpreter = struct {
if (params.len != arg_indices.len) return error.TypeMismatch;
var bind_count: usize = 0;
while (bind_count < params.len) : (bind_count += 1) {
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = arg_values[bind_count] });
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = arg_values[bind_count], .expr_idx = @enumFromInt(0) });
}
defer {
var k = params.len;
@ -1795,7 +1834,7 @@ pub const Interpreter = struct {
var bind_count: usize = 0;
while (bind_count < params.len) : (bind_count += 1) {
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = all_args[bind_count] });
try self.bindings.append(.{ .pattern_idx = params[bind_count], .value = all_args[bind_count], .expr_idx = @enumFromInt(0) });
}
defer {
var k = params.len;
@ -1816,6 +1855,17 @@ pub const Interpreter = struct {
i -= 1;
const b = self.bindings.items[i];
if (b.pattern_idx == lookup.pattern_idx) {
// Check if this binding came from an e_anno_only expression
// Skip check for expr_idx == 0 (sentinel for non-def bindings like parameters)
const expr_idx_int: u32 = @intFromEnum(b.expr_idx);
if (expr_idx_int != 0) {
const binding_expr = self.env.store.getExpr(b.expr_idx);
if (binding_expr == .e_anno_only and b.value.layout.tag != .closure) {
// This is a non-function annotation-only value being looked up
self.triggerCrash("This value has no implementation. It is only a type annotation for now.", false, roc_ops);
return error.Crash;
}
}
return try self.pushCopy(b.value, roc_ops);
}
}
@ -3287,24 +3337,25 @@ pub const Interpreter = struct {
value_rt_var: types.Var,
roc_ops: *RocOps,
out_binds: *std.array_list.AlignedManaged(Binding, null),
expr_idx: can.CIR.Expr.Idx,
) !bool {
const pat = self.env.store.getPattern(pattern_idx);
switch (pat) {
.assign => |_| {
// Bind entire value to this pattern
const copied = try self.pushCopy(value, roc_ops);
try out_binds.append(.{ .pattern_idx = pattern_idx, .value = copied });
try out_binds.append(.{ .pattern_idx = pattern_idx, .value = copied, .expr_idx = expr_idx });
return true;
},
.as => |as_pat| {
const before = out_binds.items.len;
if (!try self.patternMatchesBind(as_pat.pattern, value, value_rt_var, roc_ops, out_binds)) {
if (!try self.patternMatchesBind(as_pat.pattern, value, value_rt_var, roc_ops, out_binds, expr_idx)) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
}
const alias_value = try self.pushCopy(value, roc_ops);
try out_binds.append(.{ .pattern_idx = pattern_idx, .value = alias_value });
try out_binds.append(.{ .pattern_idx = pattern_idx, .value = alias_value, .expr_idx = expr_idx });
return true;
},
.underscore => return true,
@ -3321,11 +3372,11 @@ pub const Interpreter = struct {
},
.nominal => |n| {
const underlying = self.resolveBaseVar(value_rt_var);
return try self.patternMatchesBind(n.backing_pattern, value, underlying.var_, roc_ops, out_binds);
return try self.patternMatchesBind(n.backing_pattern, value, underlying.var_, roc_ops, out_binds, expr_idx);
},
.nominal_external => |n| {
const underlying = self.resolveBaseVar(value_rt_var);
return try self.patternMatchesBind(n.backing_pattern, value, underlying.var_, roc_ops, out_binds);
return try self.patternMatchesBind(n.backing_pattern, value, underlying.var_, roc_ops, out_binds, expr_idx);
},
.tuple => |tuple_pat| {
if (value.layout.tag != .tuple) return false;
@ -3344,7 +3395,7 @@ pub const Interpreter = struct {
if (sorted_idx >= accessor.getElementCount()) return false;
const elem_value = try accessor.getElement(sorted_idx);
const before = out_binds.items.len;
const matched = try self.patternMatchesBind(pat_ids[idx], elem_value, elem_vars[idx], roc_ops, out_binds);
const matched = try self.patternMatchesBind(pat_ids[idx], elem_value, elem_vars[idx], roc_ops, out_binds, expr_idx);
if (!matched) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
@ -3380,7 +3431,7 @@ pub const Interpreter = struct {
while (idx < prefix_len) : (idx += 1) {
const elem_value = try accessor.getElement(idx);
const before = out_binds.items.len;
const matched = try self.patternMatchesBind(non_rest_patterns[idx], elem_value, elem_rt_var, roc_ops, out_binds);
const matched = try self.patternMatchesBind(non_rest_patterns[idx], elem_value, elem_rt_var, roc_ops, out_binds, expr_idx);
if (!matched) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
@ -3393,7 +3444,7 @@ pub const Interpreter = struct {
const element_idx = total_len - suffix_len + suffix_idx;
const elem_value = try accessor.getElement(element_idx);
const before = out_binds.items.len;
const matched = try self.patternMatchesBind(suffix_pattern_idx, elem_value, elem_rt_var, roc_ops, out_binds);
const matched = try self.patternMatchesBind(suffix_pattern_idx, elem_value, elem_rt_var, roc_ops, out_binds, expr_idx);
if (!matched) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
@ -3405,7 +3456,7 @@ pub const Interpreter = struct {
const rest_value = try self.makeListSliceValue(list_layout, elem_layout, accessor.list, prefix_len, rest_len);
defer rest_value.decref(&self.runtime_layout_store, roc_ops);
const before = out_binds.items.len;
if (!try self.patternMatchesBind(rest_pat_idx, rest_value, value_rt_var, roc_ops, out_binds)) {
if (!try self.patternMatchesBind(rest_pat_idx, rest_value, value_rt_var, roc_ops, out_binds, expr_idx)) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
}
@ -3418,7 +3469,7 @@ pub const Interpreter = struct {
while (idx < non_rest_patterns.len) : (idx += 1) {
const elem_value = try accessor.getElement(idx);
const before = out_binds.items.len;
const matched = try self.patternMatchesBind(non_rest_patterns[idx], elem_value, elem_rt_var, roc_ops, out_binds);
const matched = try self.patternMatchesBind(non_rest_patterns[idx], elem_value, elem_rt_var, roc_ops, out_binds, expr_idx);
if (!matched) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
@ -3447,7 +3498,7 @@ pub const Interpreter = struct {
};
const before = out_binds.items.len;
if (!try self.patternMatchesBind(inner_pattern_idx, field_value, field_var, roc_ops, out_binds)) {
if (!try self.patternMatchesBind(inner_pattern_idx, field_value, field_var, roc_ops, out_binds, expr_idx)) {
self.trimBindingList(out_binds, before, roc_ops);
return false;
}
@ -3493,7 +3544,7 @@ pub const Interpreter = struct {
};
if (arg_patterns.len == 1) {
if (!try self.patternMatchesBind(arg_patterns[0], payload_value, arg_vars[0], roc_ops, out_binds)) {
if (!try self.patternMatchesBind(arg_patterns[0], payload_value, arg_vars[0], roc_ops, out_binds, expr_idx)) {
self.trimBindingList(out_binds, start_len, roc_ops);
return false;
}
@ -3519,7 +3570,7 @@ pub const Interpreter = struct {
return false;
}
const elem_val = try payload_tuple.getElement(sorted_idx);
if (!try self.patternMatchesBind(arg_patterns[j], elem_val, arg_vars[j], roc_ops, out_binds)) {
if (!try self.patternMatchesBind(arg_patterns[j], elem_val, arg_vars[j], roc_ops, out_binds, expr_idx)) {
self.trimBindingList(out_binds, start_len, roc_ops);
return false;
}

View file

@ -40,4 +40,5 @@ test "eval tests" {
std.testing.refAllDecls(@import("test/helpers.zig"));
std.testing.refAllDecls(@import("test/interpreter_style_test.zig"));
std.testing.refAllDecls(@import("test/interpreter_polymorphism_test.zig"));
std.testing.refAllDecls(@import("test/anno_only_interp_test.zig"));
}

View file

@ -0,0 +1,38 @@
//! Tests for e_anno_only expression evaluation in the interpreter
//!
//! NOTE: Standalone type annotations (e_anno_only) are only valid at the module/file level,
//! not in expression contexts. The current test framework (parseAndCanonicalizeExpr) parses
//! expressions, not full modules, so we cannot test this feature with unit tests here.
//!
//! The feature implementation is complete and can be tested manually with .roc files:
//!
//! Example (should crash when called):
//! foo : Str -> Str
//! result = foo "hello"
//! result
//!
//! Example (should crash when looked up):
//! bar : Str
//! result = bar
//! result
//!
//! The interpreter correctly handles:
//! - Function-typed e_anno_only: creates a closure that crashes when called (IMPLEMENTED)
//! - Non-function-typed e_anno_only: crashes when the value is looked up (IMPLEMENTED)
//!
//! Integration tests or REPL tests would be needed to verify this behavior automatically.
const std = @import("std");
const helpers = @import("helpers.zig");
const testing = std.testing;
// Placeholder test to keep the file valid
test "e_anno_only implementation exists" {
// This is just a placeholder. Real testing requires module-level code,
// which the expression-based test framework doesn't support.
// The implementation is in interpreter.zig:
// - Lines 1379-1409: e_anno_only evaluation (creates placeholder values)
// - Lines 1579-1584: function call crash check (crashes when function is called)
// - Lines 1858-1868: lookup crash check (crashes when non-function value is looked up)
try testing.expect(true);
}

View file

@ -1451,6 +1451,38 @@ pub const Tokenizer = struct {
}
},
'$' => {
const next = self.cursor.peekAt(1);
if (next) |n| {
if (n >= 'a' and n <= 'z') {
// Dollar sign followed by lowercase letter - reusable identifier
self.cursor.pos += 1;
const tag = self.cursor.chompIdentLower();
if (tag == .LowerIdent or tag == .MalformedUnicodeIdent) {
try self.pushTokenInternedHere(gpa, tag, start, start);
} else {
try self.pushTokenNormalHere(gpa, tag, start);
}
} else if (n >= 'A' and n <= 'Z') {
// Dollar sign followed by uppercase letter - reusable identifier
var tag: Token.Tag = .UpperIdent;
self.cursor.pos += 1;
if (!self.cursor.chompIdentGeneral()) {
tag = .MalformedUnicodeIdent;
}
try self.pushTokenInternedHere(gpa, tag, start, start);
} else {
// Dollar sign not followed by a letter - invalid
self.cursor.pos += 1;
try self.pushTokenNormalHere(gpa, .MalformedUnknownToken, start);
}
} else {
// Dollar sign at end of file
self.cursor.pos += 1;
try self.pushTokenNormalHere(gpa, .MalformedUnknownToken, start);
}
},
// Numbers starting with 0-9
'0'...'9' => {
const tag = self.cursor.chompNumber();
@ -2339,6 +2371,15 @@ test "tokenizer" {
.StringPart,
},
);
// Test dollar sign prefix for reusable identifiers
try testTokenization(gpa, "$foo", &[_]Token.Tag{.LowerIdent});
try testTokenization(gpa, "$Foo", &[_]Token.Tag{.UpperIdent});
try testTokenization(gpa, "$foo123", &[_]Token.Tag{.LowerIdent});
try testTokenization(gpa, "$", &[_]Token.Tag{.MalformedUnknownToken});
try testTokenization(gpa, "$123", &[_]Token.Tag{ .MalformedUnknownToken, .Int });
try testTokenization(gpa, "$ ", &[_]Token.Tag{.MalformedUnknownToken});
try testTokenization(gpa, "$foo $bar", &[_]Token.Tag{ .LowerIdent, .LowerIdent });
}
test "tokenizer with invalid UTF-8" {

View file

@ -13,9 +13,28 @@ type=expr
}
~~~
# EXPECTED
DUPLICATE DEFINITION - ann_effectful_fn.md:3:5:3:19
UNUSED VALUE - ann_effectful_fn.md:2:35:2:39
UNUSED VALUE - ann_effectful_fn.md:2:40:2:53
# PROBLEMS
**DUPLICATE DEFINITION**
The name `launchTheNukes` is being redeclared in this scope.
The redeclaration is here:
**ann_effectful_fn.md:3:5:3:19:**
```roc
launchTheNukes = |{}| ...
```
^^^^^^^^^^^^^^
But `launchTheNukes` was already defined here:
**ann_effectful_fn.md:2:5:2:34:**
```roc
launchTheNukes : {} => Result Bool LaunchNukeErr
```
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**UNUSED VALUE**
This expression produces a value, but it's not being used:
**ann_effectful_fn.md:2:35:2:39:**
@ -81,10 +100,9 @@ EndOfFile,
# CANONICALIZE
~~~clojure
(e-block
(s-type-anno (name "launchTheNukes")
(ty-fn (effectful true)
(ty-record)
(ty-lookup (name "Result") (external-module "Result"))))
(s-let
(p-assign (ident "launchTheNukes"))
(e-anno-only))
(s-expr
(e-tag (name "Bool")))
(s-expr

View file

@ -22,6 +22,8 @@ MALFORMED TYPE - record_builder.md:2:8:2:9
UNRECOGNIZED SYNTAX - record_builder.md:2:9:2:10
MALFORMED TYPE - record_builder.md:3:8:3:9
UNRECOGNIZED SYNTAX - record_builder.md:3:9:3:10
UNUSED VARIABLE - record_builder.md:2:5:2:9
UNUSED VARIABLE - record_builder.md:3:5:3:9
# PROBLEMS
**UNEXPECTED TOKEN IN EXPRESSION**
The token **<-** is not expected in an expression.
@ -142,6 +144,30 @@ I don't recognize this syntax.
This might be a syntax error, an unsupported language feature, or a typo.
**UNUSED VARIABLE**
Variable `x` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_x` to suppress this warning.
The unused variable is declared here:
**record_builder.md:2:5:2:9:**
```roc
x: 5,
```
^^^^
**UNUSED VARIABLE**
Variable `y` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_y` to suppress this warning.
The unused variable is declared here:
**record_builder.md:3:5:3:9:**
```roc
y: 0,
```
^^^^
# TOKENS
~~~zig
OpenCurly,UpperIdent,NoSpaceDotUpperIdent,NoSpaceDotLowerIdent,OpBackArrow,
@ -181,12 +207,14 @@ EndOfFile,
(e-runtime-error (tag "ident_not_in_scope")))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "x")
(ty-malformed))
(s-let
(p-assign (ident "x"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "y")
(ty-malformed))
(s-let
(p-assign (ident "y"))
(e-anno-only))
(e-runtime-error (tag "expr_not_canonicalized")))
~~~
# TYPES

View file

@ -13,6 +13,7 @@ UNEXPECTED TOKEN IN TYPE ANNOTATION - record_field_update_error.md:1:17:1:19
UNDEFINED VARIABLE - record_field_update_error.md:1:3:1:9
UNRECOGNIZED SYNTAX - record_field_update_error.md:1:10:1:11
MALFORMED TYPE - record_field_update_error.md:1:17:1:19
UNUSED VARIABLE - record_field_update_error.md:1:12:1:19
# PROBLEMS
**UNEXPECTED TOKEN IN EXPRESSION**
The token **&** is not expected in an expression.
@ -68,6 +69,18 @@ This type annotation is malformed or contains invalid syntax.
^^
**UNUSED VARIABLE**
Variable `age` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_age` to suppress this warning.
The unused variable is declared here:
**record_field_update_error.md:1:12:1:19:**
```roc
{ person & age: 31 }
```
^^^^^^^
# TOKENS
~~~zig
OpenCurly,LowerIdent,OpAmpersand,LowerIdent,OpColon,Int,CloseCurly,
@ -96,8 +109,9 @@ EndOfFile,
(e-runtime-error (tag "ident_not_in_scope")))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "age")
(ty-malformed))
(s-let
(p-assign (ident "age"))
(e-anno-only))
(e-empty_record))
~~~
# TYPES

View file

@ -45,6 +45,7 @@ main = |_| {
# EXPECTED
UNEXPECTED TOKEN IN EXPRESSION - let_polymorphism_records.md:19:50:19:51
UNRECOGNIZED SYNTAX - let_polymorphism_records.md:19:50:19:51
UNUSED VARIABLE - let_polymorphism_records.md:19:52:19:67
UNUSED VARIABLE - let_polymorphism_records.md:19:27:19:36
UNUSED VALUE - let_polymorphism_records.md:19:40:19:49
# PROBLEMS
@ -70,6 +71,18 @@ update_data = |container, new_value| { container & data: new_value }
This might be a syntax error, an unsupported language feature, or a typo.
**UNUSED VARIABLE**
Variable `data` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_data` to suppress this warning.
The unused variable is declared here:
**let_polymorphism_records.md:19:52:19:67:**
```roc
update_data = |container, new_value| { container & data: new_value }
```
^^^^^^^^^^^^^^^
**UNUSED VARIABLE**
Variable `new_value` is not used anywhere in your code.
@ -353,8 +366,9 @@ main = |_| {
(p-assign (ident "container"))))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "data")
(ty-rigid-var (name "new_value")))
(s-let
(p-assign (ident "data"))
(e-anno-only))
(e-empty_record))))
(d-let
(p-assign (ident "updated_int"))

View file

@ -12,6 +12,7 @@ UNEXPECTED TOKEN IN TYPE ANNOTATION - error_malformed_syntax_2.md:1:8:1:10
UNEXPECTED TOKEN IN EXPRESSION - error_malformed_syntax_2.md:1:10:1:11
MALFORMED TYPE - error_malformed_syntax_2.md:1:8:1:10
UNRECOGNIZED SYNTAX - error_malformed_syntax_2.md:1:10:1:11
UNUSED VARIABLE - error_malformed_syntax_2.md:1:3:1:10
UNUSED VARIABLE - error_malformed_syntax_2.md:1:12:1:16
# PROBLEMS
**UNEXPECTED TOKEN IN TYPE ANNOTATION**
@ -57,6 +58,18 @@ I don't recognize this syntax.
This might be a syntax error, an unsupported language feature, or a typo.
**UNUSED VARIABLE**
Variable `age` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_age` to suppress this warning.
The unused variable is declared here:
**error_malformed_syntax_2.md:1:3:1:10:**
```roc
{ age: 42, name = "Alice" }
```
^^^^^^^
**UNUSED VARIABLE**
Variable `name` is not used anywhere in your code.
@ -96,8 +109,9 @@ EndOfFile,
# CANONICALIZE
~~~clojure
(e-block
(s-type-anno (name "age")
(ty-malformed))
(s-let
(p-assign (ident "age"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-let

View file

@ -27,7 +27,6 @@ UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:4:15:4:16
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:4:25:4:26
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:5:15:5:16
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:5:24:5:25
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:6:10:6:11
UNEXPECTED TOKEN IN TYPE ANNOTATION - record_different_fields_error.md:6:20:6:21
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:6:21:6:27
UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_error.md:6:27:6:28
@ -50,7 +49,6 @@ UNDEFINED VARIABLE - record_different_fields_error.md:5:11:5:15
UNRECOGNIZED SYNTAX - record_different_fields_error.md:5:15:5:16
UNRECOGNIZED SYNTAX - record_different_fields_error.md:5:24:5:25
UNDEFINED VARIABLE - record_different_fields_error.md:6:5:6:10
UNRECOGNIZED SYNTAX - record_different_fields_error.md:6:10:6:11
MALFORMED TYPE - record_different_fields_error.md:6:20:6:21
UNRECOGNIZED SYNTAX - record_different_fields_error.md:6:21:6:27
UNRECOGNIZED SYNTAX - record_different_fields_error.md:6:27:6:28
@ -59,6 +57,8 @@ UNDEFINED VARIABLE - record_different_fields_error.md:7:5:7:10
UNRECOGNIZED SYNTAX - record_different_fields_error.md:7:10:7:17
UNRECOGNIZED SYNTAX - record_different_fields_error.md:7:17:7:18
UNRECOGNIZED SYNTAX - record_different_fields_error.md:7:30:7:31
UNUSED VARIABLE - record_different_fields_error.md:3:5:3:14
UNUSED VARIABLE - record_different_fields_error.md:6:10:6:21
UNUSED VALUE - record_different_fields_error.md:4:5:4:15
UNUSED VALUE - record_different_fields_error.md:4:17:4:25
UNUSED VALUE - record_different_fields_error.md:5:17:5:24
@ -196,17 +196,6 @@ Expressions can be identifiers, literals, function calls, or operators.
^
**UNEXPECTED TOKEN IN EXPRESSION**
The token **$** is not expected in an expression.
Expressions can be identifiers, literals, function calls, or operators.
**record_different_fields_error.md:6:10:6:11:**
```roc
field$special: "dollar",
```
^
**UNEXPECTED TOKEN IN TYPE ANNOTATION**
The token **"** is not expected in a type annotation.
Type annotations should contain types like _Str_, _Num a_, or _List U64_.
@ -447,17 +436,6 @@ Is there an `import` or `exposing` missing up-top?
^^^^^
**UNRECOGNIZED SYNTAX**
I don't recognize this syntax.
**record_different_fields_error.md:6:10:6:11:**
```roc
field$special: "dollar",
```
^
This might be a syntax error, an unsupported language feature, or a typo.
**MALFORMED TYPE**
This type annotation is malformed or contains invalid syntax.
@ -545,6 +523,30 @@ I don't recognize this syntax.
This might be a syntax error, an unsupported language feature, or a typo.
**UNUSED VARIABLE**
Variable `field_` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_field_` to suppress this warning.
The unused variable is declared here:
**record_different_fields_error.md:3:5:3:14:**
```roc
field_: "trailing underscore",
```
^^^^^^^^^
**UNUSED VARIABLE**
Variable `$special` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_$special` to suppress this warning.
The unused variable is declared here:
**record_different_fields_error.md:6:10:6:21:**
```roc
field$special: "dollar",
```
^^^^^^^^^^^
**UNUSED VALUE**
This expression produces a value, but it's not being used:
**record_different_fields_error.md:4:5:4:15:**
@ -596,7 +598,7 @@ NamedUnderscore,OpColon,StringStart,StringPart,StringEnd,Comma,
LowerIdent,OpColon,StringStart,StringPart,StringEnd,Comma,
UpperIdent,OpColon,StringStart,StringPart,StringEnd,Comma,
LowerIdent,OpUnaryMinus,LowerIdent,OpColon,StringStart,StringPart,StringEnd,Comma,
LowerIdent,MalformedUnknownToken,LowerIdent,OpColon,StringStart,StringPart,StringEnd,Comma,
LowerIdent,LowerIdent,OpColon,StringStart,StringPart,StringEnd,Comma,
LowerIdent,OpaqueName,OpColon,StringStart,StringPart,StringEnd,Comma,
CloseCurly,
EndOfFile,
@ -628,8 +630,7 @@ EndOfFile,
(e-string-part (raw "kebab")))
(e-malformed (reason "expr_unexpected_token"))
(e-ident (raw "field"))
(e-malformed (reason "expr_unexpected_token"))
(s-type-anno (name "special")
(s-type-anno (name "$special")
(ty-malformed (tag "ty_anno_unexpected_token")))
(e-malformed (reason "expr_unexpected_token"))
(e-malformed (reason "expr_unexpected_token"))
@ -656,7 +657,7 @@ EndOfFile,
"kebab"
field
special :
$special :
field
"at symbol"
@ -666,16 +667,18 @@ EndOfFile,
# CANONICALIZE
~~~clojure
(e-block
(s-type-anno (name "_privateField")
(ty-malformed))
(s-let
(p-assign (ident "_privateField"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "field_")
(ty-malformed))
(s-let
(p-assign (ident "field_"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr
@ -705,10 +708,9 @@ EndOfFile,
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr
(e-runtime-error (tag "ident_not_in_scope")))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "special")
(ty-malformed))
(s-let
(p-assign (ident "$special"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr

View file

@ -49,6 +49,7 @@ UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:7:5:7:7
UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:7:7:7:8
DOES NOT EXIST - record_different_fields_reserved_error.md:7:9:7:19
UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:7:19:7:20
UNUSED VARIABLE - record_different_fields_reserved_error.md:3:5:3:12
UNUSED VALUE - record_different_fields_reserved_error.md:4:13:4:29
UNUSED VALUE - record_different_fields_reserved_error.md:5:13:5:26
# PROBLEMS
@ -424,6 +425,18 @@ I don't recognize this syntax.
This might be a syntax error, an unsupported language feature, or a typo.
**UNUSED VARIABLE**
Variable `when` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_when` to suppress this warning.
The unused variable is declared here:
**record_different_fields_reserved_error.md:3:5:3:12:**
```roc
when: "pattern match",
```
^^^^^^^
**UNUSED VALUE**
This expression produces a value, but it's not being used:
**record_different_fields_reserved_error.md:4:13:4:29:**
@ -509,8 +522,9 @@ EndOfFile,
(e-block
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-type-anno (name "when")
(ty-malformed))
(s-let
(p-assign (ident "when"))
(e-anno-only))
(s-expr
(e-runtime-error (tag "expr_not_canonicalized")))
(s-expr

View file

@ -67,14 +67,9 @@ process_user! : { name : Str, age : } => Str
# CANONICALIZE
~~~clojure
(can-ir
(s-type-anno (name "process_user!")
(ty-fn (effectful true)
(ty-record
(field (field "name")
(ty-lookup (name "Str") (external-module "Str")))
(field (field "age")
(ty-malformed)))
(ty-lookup (name "Str") (external-module "Str")))))
(s-let
(p-assign (ident "process_user!"))
(e-anno-only)))
~~~
# TYPES
~~~clojure

View file

@ -39,19 +39,9 @@ NO CHANGE
# CANONICALIZE
~~~clojure
(can-ir
(s-type-anno (name "create_user!")
(ty-fn (effectful true)
(ty-lookup (name "Str") (external-module "Str"))
(ty-lookup (name "U32") (builtin))
(ty-record
(field (field "name")
(ty-lookup (name "Str") (external-module "Str")))
(field (field "age")
(ty-lookup (name "U32") (builtin)))
(field (field "id")
(ty-lookup (name "U64") (builtin)))
(field (field "active")
(ty-lookup (name "Bool") (external-module "Bool")))))))
(s-let
(p-assign (ident "create_user!"))
(e-anno-only)))
~~~
# TYPES
~~~clojure

View file

@ -73,8 +73,9 @@ process_user! :
# CANONICALIZE
~~~clojure
(can-ir
(s-type-anno (name "process_user!")
(ty-malformed)))
(s-let
(p-assign (ident "process_user!"))
(e-anno-only)))
~~~
# TYPES
~~~clojure

View file

@ -39,20 +39,9 @@ process_things : { name : Str, age : U32, thing : a }, (a -> Str) -> Str
# CANONICALIZE
~~~clojure
(can-ir
(s-type-anno (name "process_things")
(ty-fn (effectful false)
(ty-record
(field (field "name")
(ty-lookup (name "Str") (external-module "Str")))
(field (field "age")
(ty-lookup (name "U32") (builtin)))
(field (field "thing")
(ty-rigid-var (name "a"))))
(ty-parens
(ty-fn (effectful false)
(ty-rigid-var-lookup (ty-rigid-var (name "a")))
(ty-lookup (name "Str") (external-module "Str"))))
(ty-lookup (name "Str") (external-module "Str")))))
(s-let
(p-assign (ident "process_things"))
(e-anno-only)))
~~~
# TYPES
~~~clojure

View file

@ -1,6 +1,6 @@
# META
~~~ini
description=Comprehensive test of type variable underscore conventions
description=Comprehensive test of type variable underscore and dollar sign conventions
type=file
~~~
# SOURCE
@ -11,12 +11,12 @@ app [main] { pf: platform "../basic-cli/platform.roc" }
single_use : List(elem) -> Str
single_use = |x| "hello"
# Test 2: TYPE VAR ENDING IN UNDERSCORE - variables should never end with underscore
ending_underscore : List(elem_) -> elem_
ending_underscore = |list| "default"
# Test 2: TYPE VAR STARTING WITH DOLLAR - variables should never start with dollar sign (reusable markers)
starting_dollar : List($elem) -> $elem
starting_dollar = |list| "default"
# Test 3: COMBINATION - single-use ending in underscore (both errors)
combo_single : List(bad_) -> Str
# Test 3: COMBINATION - single-use starting with dollar (both errors)
combo_single : List($bad) -> Str
combo_single = |x| "combo"
# Test 4: VALID CASES - these should not generate warnings
@ -30,11 +30,11 @@ main = |x| "done"
~~~
# EXPECTED
UNUSED VARIABLE - type_var_underscore_conventions.md:5:15:5:16
UNUSED VARIABLE - type_var_underscore_conventions.md:9:22:9:26
UNUSED VARIABLE - type_var_underscore_conventions.md:9:20:9:24
UNUSED VARIABLE - type_var_underscore_conventions.md:13:17:13:18
UNUSED VARIABLE - type_var_underscore_conventions.md:17:17:17:18
UNUSED VARIABLE - type_var_underscore_conventions.md:22:9:22:10
TYPE MISMATCH - type_var_underscore_conventions.md:9:28:9:37
TYPE MISMATCH - type_var_underscore_conventions.md:9:26:9:35
# PROBLEMS
**UNUSED VARIABLE**
Variable `x` is not used anywhere in your code.
@ -53,11 +53,11 @@ Variable `list` is not used anywhere in your code.
If you don't need this variable, prefix it with an underscore like `_list` to suppress this warning.
The unused variable is declared here:
**type_var_underscore_conventions.md:9:22:9:26:**
**type_var_underscore_conventions.md:9:20:9:24:**
```roc
ending_underscore = |list| "default"
starting_dollar = |list| "default"
```
^^^^
^^^^
**UNUSED VARIABLE**
@ -98,17 +98,17 @@ main = |x| "done"
**TYPE MISMATCH**
This expression is used in an unexpected way:
**type_var_underscore_conventions.md:9:28:9:37:**
**type_var_underscore_conventions.md:9:26:9:35:**
```roc
ending_underscore = |list| "default"
starting_dollar = |list| "default"
```
^^^^^^^^^
^^^^^^^^^
It has the type:
_Str_
But the type annotation says it should have the type:
_elem__
_$elem_
# TOKENS
~~~zig
@ -154,14 +154,14 @@ EndOfFile,
(p-ident (raw "x")))
(e-string
(e-string-part (raw "hello")))))
(s-type-anno (name "ending_underscore")
(s-type-anno (name "starting_dollar")
(ty-fn
(ty-apply
(ty (name "List"))
(ty-var (raw "elem_")))
(ty-var (raw "elem_"))))
(ty-var (raw "$elem")))
(ty-var (raw "$elem"))))
(s-decl
(p-ident (raw "ending_underscore"))
(p-ident (raw "starting_dollar"))
(e-lambda
(args
(p-ident (raw "list")))
@ -171,7 +171,7 @@ EndOfFile,
(ty-fn
(ty-apply
(ty (name "List"))
(ty-var (raw "bad_")))
(ty-var (raw "$bad")))
(ty (name "Str"))))
(s-decl
(p-ident (raw "combo_single"))
@ -234,7 +234,7 @@ NO CHANGE
(ty-rigid-var (name "elem")))
(ty-lookup (name "Str") (external-module "Str")))))
(d-let
(p-assign (ident "ending_underscore"))
(p-assign (ident "starting_dollar"))
(e-lambda
(args
(p-assign (ident "list")))
@ -243,8 +243,8 @@ NO CHANGE
(annotation
(ty-fn (effectful false)
(ty-apply (name "List") (builtin)
(ty-rigid-var (name "elem_")))
(ty-rigid-var-lookup (ty-rigid-var (name "elem_"))))))
(ty-rigid-var (name "$elem")))
(ty-rigid-var-lookup (ty-rigid-var (name "$elem"))))))
(d-let
(p-assign (ident "combo_single"))
(e-lambda
@ -255,7 +255,7 @@ NO CHANGE
(annotation
(ty-fn (effectful false)
(ty-apply (name "List") (builtin)
(ty-rigid-var (name "bad_")))
(ty-rigid-var (name "$bad")))
(ty-lookup (name "Str") (external-module "Str")))))
(d-let
(p-assign (ident "valid_single"))
@ -296,15 +296,15 @@ NO CHANGE
(inferred-types
(defs
(patt (type "List(elem) -> Str"))
(patt (type "List(elem_) -> Error"))
(patt (type "List(bad_) -> Str"))
(patt (type "List($elem) -> Error"))
(patt (type "List($bad) -> Str"))
(patt (type "List(_elem) -> Str"))
(patt (type "elem -> List(elem)"))
(patt (type "_arg -> Str")))
(expressions
(expr (type "List(elem) -> Str"))
(expr (type "List(elem_) -> Error"))
(expr (type "List(bad_) -> Str"))
(expr (type "List($elem) -> Error"))
(expr (type "List($bad) -> Str"))
(expr (type "List(_elem) -> Str"))
(expr (type "elem -> List(elem)"))
(expr (type "_arg -> Str"))))