Skip to content

Commit f880f64

Browse files
committed
Introduce .e_hosted_lambda
1 parent ea7420d commit f880f64

File tree

15 files changed

+229
-80
lines changed

15 files changed

+229
-80
lines changed

src/canonicalize/Can.zig

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const Token = tokenize.Token;
2323
const DataSpan = base.DataSpan;
2424
const ModuleEnv = @import("ModuleEnv.zig");
2525
const Node = @import("Node.zig");
26+
const HostedCompiler = @import("HostedCompiler.zig");
2627

2728
/// Information about an auto-imported module type
2829
pub const AutoImportedType = struct {
@@ -1255,6 +1256,31 @@ pub fn canonicalizeFile(
12551256
self.env.all_defs = try self.env.store.defSpanFrom(scratch_defs_start);
12561257
self.env.all_statements = try self.env.store.statementSpanFrom(scratch_statements_start);
12571258

1259+
// For Type Modules, transform annotation-only defs into hosted lambdas
1260+
// This allows platforms to import these modules and use the hosted functions
1261+
if (self.env.module_kind == .type_module) {
1262+
var new_def_indices = try HostedCompiler.replaceAnnoOnlyWithHosted(self.env);
1263+
defer new_def_indices.deinit(self.env.gpa);
1264+
1265+
if (new_def_indices.items.len > 0) {
1266+
// Rebuild all_defs span to include both old and new defs
1267+
const old_defs = self.env.store.sliceDefs(self.env.all_defs);
1268+
const defs_start = self.env.store.scratchTop("defs");
1269+
1270+
// Add all old defs
1271+
for (old_defs) |def_idx| {
1272+
try self.env.store.scratch.?.defs.append(def_idx);
1273+
}
1274+
1275+
// Add all new defs
1276+
for (new_def_indices.items) |def_idx| {
1277+
try self.env.store.scratch.?.defs.append(def_idx);
1278+
}
1279+
1280+
self.env.all_defs = try self.env.store.defSpanFrom(defs_start);
1281+
}
1282+
}
1283+
12581284
// Create the span of exported defs by finding definitions that correspond to exposed items
12591285
try self.populateExports();
12601286

src/canonicalize/DependencyGraph.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ fn collectExprDependencies(
245245
try collectExprDependencies(cir, ll.body, dependencies, allocator);
246246
},
247247

248+
.e_hosted_lambda => |hosted| {
249+
try collectExprDependencies(cir, hosted.body, dependencies, allocator);
250+
},
251+
248252
// External lookups reference other modules - skip for now
249253
.e_lookup_external => {},
250254

src/canonicalize/Expression.zig

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,17 @@ pub const Expr = union(enum) {
382382
body: Expr.Idx,
383383
},
384384

385+
/// A hosted lambda that will be provided by the host application at runtime.
386+
/// ```roc
387+
/// # In a hosted module:
388+
/// putStdout! : Str => {}
389+
/// ```
390+
e_hosted_lambda: struct {
391+
symbol_name: base.Ident.Idx,
392+
args: CIR.Pattern.Span,
393+
body: Expr.Idx,
394+
},
395+
385396
/// Low-level builtin operations that are implemented by the compiler backend.
386397
pub const LowLevel = enum {
387398
str_is_empty,
@@ -1053,6 +1064,27 @@ pub const Expr = union(enum) {
10531064

10541065
try tree.endNode(begin, attrs);
10551066
},
1067+
.e_hosted_lambda => |hosted| {
1068+
const begin = tree.beginNode();
1069+
try tree.pushStaticAtom("e-hosted-lambda");
1070+
const symbol_name = ir.common.getIdent(hosted.symbol_name);
1071+
try tree.pushStringPair("symbol", symbol_name);
1072+
const region = ir.store.getExprRegion(expr_idx);
1073+
try ir.appendRegionInfoToSExprTreeFromRegion(tree, region);
1074+
const attrs = tree.beginNode();
1075+
1076+
const args_begin = tree.beginNode();
1077+
try tree.pushStaticAtom("args");
1078+
const args_attrs = tree.beginNode();
1079+
for (ir.store.slicePatterns(hosted.args)) |arg_idx| {
1080+
try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx);
1081+
}
1082+
try tree.endNode(args_begin, args_attrs);
1083+
1084+
try ir.store.getExpr(hosted.body).pushToSExprTree(ir, tree, hosted.body);
1085+
1086+
try tree.endNode(begin, attrs);
1087+
},
10561088
.e_crash => |e| {
10571089
const begin = tree.beginNode();
10581090
try tree.pushStaticAtom("e-crash");
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const std = @import("std");
2+
const base = @import("base");
3+
const ModuleEnv = @import("ModuleEnv.zig");
4+
const CIR = @import("CIR.zig");
5+
6+
/// Replace all e_anno_only expressions in a hosted module with e_hosted_lambda operations.
7+
/// This transforms standalone annotations into hosted lambda operations that will be
8+
/// provided by the host application at runtime.
9+
/// Returns a list of new def indices created.
10+
pub fn replaceAnnoOnlyWithHosted(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
11+
const gpa = env.gpa;
12+
var new_def_indices = std.ArrayList(CIR.Def.Idx).empty;
13+
14+
// Ensure types array has entries for all existing nodes
15+
// This is necessary because varFrom(node_idx) assumes type_var index == node index
16+
const current_nodes = env.store.nodes.len();
17+
const current_types = env.types.len();
18+
if (current_types < current_nodes) {
19+
// Fill the gap with fresh type variables
20+
var i: u64 = current_types;
21+
while (i < current_nodes) : (i += 1) {
22+
_ = env.types.fresh() catch unreachable;
23+
}
24+
}
25+
26+
// Iterate through all defs and replace ALL anno-only defs with hosted implementations
27+
const all_defs = env.store.sliceDefs(env.all_defs);
28+
for (all_defs) |def_idx| {
29+
const def = env.store.getDef(def_idx);
30+
const expr = env.store.getExpr(def.expr);
31+
32+
// Check if this is an anno-only def (e_anno_only expression)
33+
if (expr == .e_anno_only and def.annotation != null) {
34+
// Get the identifier from the pattern
35+
const pattern = env.store.getPattern(def.pattern);
36+
if (pattern == .assign) {
37+
const ident = pattern.assign.ident;
38+
39+
// Create a dummy parameter pattern for the lambda
40+
// Use the identifier "_arg" for the parameter
41+
const arg_ident = env.common.findIdent("_arg") orelse try env.common.insertIdent(gpa, base.Ident.for_text("_arg"));
42+
const arg_pattern_idx = try env.addPattern(.{ .assign = .{ .ident = arg_ident } }, base.Region.zero());
43+
44+
// Create a pattern span containing just this one parameter
45+
const patterns_start = env.store.scratchTop("patterns");
46+
try env.store.scratch.?.patterns.append(arg_pattern_idx);
47+
const args_span = CIR.Pattern.Span{ .span = .{ .start = @intCast(patterns_start), .len = 1 } };
48+
49+
// Create an e_runtime_error body that crashes when the function is called in the interpreter
50+
const error_msg_lit = try env.insertString("Hosted functions cannot be called in the interpreter");
51+
const diagnostic_idx = try env.addDiagnostic(.{ .not_implemented = .{
52+
.feature = error_msg_lit,
53+
.region = base.Region.zero(),
54+
} });
55+
const body_idx = try env.addExpr(.{ .e_runtime_error = .{ .diagnostic = diagnostic_idx } }, base.Region.zero());
56+
57+
// Create e_hosted_lambda expression
58+
const expr_idx = try env.addExpr(.{ .e_hosted_lambda = .{
59+
.symbol_name = ident,
60+
.args = args_span,
61+
.body = body_idx,
62+
} }, base.Region.zero());
63+
64+
// Now replace the e_anno_only expression with the e_hosted_lambda
65+
// We need to modify the def's expr field to point to our new expression
66+
// CIR.Def.Idx and Node.Idx have the same underlying representation
67+
const def_node_idx = @as(@TypeOf(env.store.nodes).Idx, @enumFromInt(@intFromEnum(def_idx)));
68+
var def_node = env.store.nodes.get(def_node_idx);
69+
def_node.data_2 = @intFromEnum(expr_idx);
70+
env.store.nodes.set(def_node_idx, def_node);
71+
72+
// Track this replaced def index
73+
try new_def_indices.append(gpa, def_idx);
74+
}
75+
}
76+
}
77+
78+
return new_def_indices;
79+
}

src/canonicalize/Node.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub const Tag = enum {
7979
expr_ellipsis,
8080
expr_anno_only,
8181
expr_low_level,
82+
expr_hosted,
8283
expr_expect,
8384
expr_record_builder,
8485
match_branch,

src/canonicalize/NodeStore.zig

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub fn deinit(store: *NodeStore) void {
130130
/// Count of the diagnostic nodes in the ModuleEnv
131131
pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59;
132132
/// Count of the expression nodes in the ModuleEnv
133-
pub const MODULEENV_EXPR_NODE_COUNT = 35;
133+
pub const MODULEENV_EXPR_NODE_COUNT = 36;
134134
/// Count of the statement nodes in the ModuleEnv
135135
pub const MODULEENV_STATEMENT_NODE_COUNT = 14;
136136
/// Count of the type annotation nodes in the ModuleEnv
@@ -647,6 +647,22 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr {
647647
.body = @enumFromInt(body_idx),
648648
} };
649649
},
650+
.expr_hosted => {
651+
// Retrieve hosted lambda data from extra_data
652+
const symbol_name: base.Ident.Idx = @bitCast(node.data_1);
653+
const extra_start = node.data_2;
654+
const extra_data = store.extra_data.items.items[extra_start..];
655+
656+
const args_start = extra_data[0];
657+
const args_len = extra_data[1];
658+
const body_idx = extra_data[2];
659+
660+
return CIR.Expr{ .e_hosted_lambda = .{
661+
.symbol_name = symbol_name,
662+
.args = .{ .span = .{ .start = args_start, .len = args_len } },
663+
.body = @enumFromInt(body_idx),
664+
} };
665+
},
650666
.expr_expect => {
651667
return CIR.Expr{ .e_expect = .{
652668
.body = @enumFromInt(node.data_1),
@@ -1517,6 +1533,22 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator
15171533

15181534
node.data_2 = @intCast(extra_data_start);
15191535
},
1536+
.e_hosted_lambda => |hosted| {
1537+
node.tag = .expr_hosted;
1538+
node.data_1 = @bitCast(hosted.symbol_name);
1539+
1540+
// Store hosted lambda data in extra_data
1541+
const extra_data_start = store.extra_data.len();
1542+
1543+
// Store args span start
1544+
_ = try store.extra_data.append(store.gpa, hosted.args.span.start);
1545+
// Store args span length
1546+
_ = try store.extra_data.append(store.gpa, hosted.args.span.len);
1547+
// Store body index
1548+
_ = try store.extra_data.append(store.gpa, @intFromEnum(hosted.body));
1549+
1550+
node.data_2 = @intCast(extra_data_start);
1551+
},
15201552
.e_match => |e| {
15211553
node.tag = .expr_match;
15221554

src/check/Check.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3005,6 +3005,21 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected
30053005
},
30063006
}
30073007
},
3008+
.e_hosted_lambda => |hosted| {
3009+
// For hosted lambda expressions, treat like a lambda with a crash body.
3010+
// Check the body (which will be e_runtime_error or similar)
3011+
does_fx = try self.checkExpr(hosted.body, rank, .no_expectation) or does_fx;
3012+
3013+
// The lambda's type comes from the annotation.
3014+
// Like e_anno_only and e_low_level_lambda, this should always have an annotation.
3015+
// The type will be unified with the expected type in the code below.
3016+
switch (expected) {
3017+
.no_expectation => unreachable,
3018+
.expected => {
3019+
// The expr_var will be unified with the annotation var below
3020+
},
3021+
}
3022+
},
30083023
.e_runtime_error => {
30093024
try self.updateVar(expr_var, .err, rank);
30103025
},

test/HostedIO.roc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
HostedIO := [].{
2+
## Print a string to stdout
3+
put_line! : Str => {}
4+
5+
## Read a line from stdin
6+
get_line! : {} => Str
7+
8+
## Write to a file
9+
write_file! : Str, Str => Try({}, [FileWriteError])
10+
}

test/fx/app.roc

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
1-
app [main!, putStdout!, putStderr!] { pf: platform "./platform/main.roc" }
1+
app [main!] { pf: platform "./platform/main.roc" }
22

3-
main! : () => {}
4-
main! = ||
5-
putStdout!("Hello from stdout!")
6-
putStderr!("Error from stderr!")
7-
putStdout!("Line 1 to stdout")
8-
putStderr!("Line 2 to stderr")
9-
putStdout!("Line 3 to stdout")
10-
{}
3+
import pf.Stdout
114

12-
putStdout! : Str => {}
13-
putStdout! = |_msg|
14-
# In a full implementation, this would call a host-provided function
15-
# For now, the host will intercept these calls and perform the actual I/O
16-
{}
17-
18-
putStderr! : Str => {}
19-
putStderr! = |_msg|
20-
# In a full implementation, this would call a host-provided function
21-
# For now, the host will intercept these calls and perform the actual I/O
22-
{}
5+
main! = |_| Stdout.line!("Hello from stdout!")

test/fx/platform/Host.roc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Host := [].{
2+
put_stdout! : Str => {}
3+
put_stderr! : Str => {}
4+
}

0 commit comments

Comments
 (0)