Skip to content

Commit 12cb41c

Browse files
brielovclaude
andcommitted
Require explicit module qualifier for do-notation: do[Module]
Do-notation now requires an explicit module qualifier that specifies which module's flatMap to use. This eliminates the need to manually bring flatMap into scope before using do-notation. Syntax: do[Module] ... end (e.g., do[Maybe] x <- Just 1; Just x end) Changes: - Add moduleName field to SDo type in surface AST - Parser requires do[Module] syntax (not optional) - Desugarer generates Module.flatMap calls - Update integration tests to use constructors directly (no pure) - Update VSCode syntax highlighting for do[Module] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1dc8854 commit 12cb41c

6 files changed

Lines changed: 69 additions & 39 deletions

File tree

editors/vscode/syntaxes/algow.tmLanguage.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
{ "include": "#strings" },
88
{ "include": "#characters" },
99
{ "include": "#numbers" },
10+
{ "include": "#do-notation" },
1011
{ "include": "#keywords" },
1112
{ "include": "#operators" },
1213
{ "include": "#types" },
@@ -29,6 +30,15 @@
2930
}
3031
]
3132
},
33+
"do-notation": {
34+
"match": "\\b(do)\\s*(\\[)\\s*([A-Z][a-zA-Z0-9_]*)\\s*(\\])",
35+
"captures": {
36+
"1": { "name": "keyword.control.do.algow" },
37+
"2": { "name": "punctuation.bracket.square.algow" },
38+
"3": { "name": "entity.name.type.module.algow" },
39+
"4": { "name": "punctuation.bracket.square.algow" }
40+
}
41+
},
3242
"strings": {
3343
"name": "string.quoted.double.algow",
3444
"begin": "\"",
@@ -60,7 +70,7 @@
6070
"patterns": [
6171
{
6272
"name": "keyword.control.algow",
63-
"match": "\\b(if|then|else|match|when|end|do)\\b"
73+
"match": "\\b(if|then|else|match|when|end)\\b"
6474
},
6575
{
6676
"name": "keyword.other.algow",

integration/do-notation/main.alg

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22
-- Do-Notation Examples
33
-- =============================================================================
44
-- Demonstrates do-notation for monadic composition
5-
6-
-- Bring Maybe's flatMap into scope for do-notation
7-
let flatMap = Maybe.flatMap
8-
let pure = x -> Just x
5+
-- Syntax: do[Module] ... end uses Module.flatMap for bind operations
96

107
-- Basic do-notation: sequence Maybe computations
118
let testBasic =
12-
let result = do
9+
let result = do[Maybe]
1310
x <- Just 1
1411
y <- Just 2
1512
z <- Just 3
16-
pure (x + y + z)
13+
Just (x + y + z)
1714
end in
1815
match result
1916
when Just n -> n
@@ -22,11 +19,11 @@ let testBasic =
2219

2320
-- Short-circuit on Nothing
2421
let testShortCircuit =
25-
let result = do
22+
let result = do[Maybe]
2623
x <- Just 10
2724
y <- Nothing
2825
z <- Just 20
29-
pure (x + y + z)
26+
Just (x + y + z)
3027
end in
3128
match result
3229
when Just _ -> 0
@@ -35,10 +32,10 @@ let testShortCircuit =
3532

3633
-- Pattern matching in bind
3734
let testPatternBind =
38-
let result = do
35+
let result = do[Maybe]
3936
(a, b) <- Just (1, 2)
4037
(c, d) <- Just (3, 4)
41-
pure (a + b + c + d)
38+
Just (a + b + c + d)
4239
end in
4340
match result
4441
when Just n -> n
@@ -47,11 +44,11 @@ let testPatternBind =
4744

4845
-- Let bindings inside do
4946
let testDoLet =
50-
let result = do
47+
let result = do[Maybe]
5148
x <- Just 5
5249
let doubled = x * 2
5350
y <- Just doubled
54-
pure (x + y)
51+
Just (x + y)
5552
end in
5653
match result
5754
when Just n -> n
@@ -60,14 +57,14 @@ let testDoLet =
6057

6158
-- Nested do blocks
6259
let testNested =
63-
let inner = x -> do
60+
let inner = x -> do[Maybe]
6461
y <- Just (x * 2)
65-
pure (y + 1)
62+
Just (y + 1)
6663
end in
67-
let result = do
64+
let result = do[Maybe]
6865
a <- Just 5
6966
b <- inner a
70-
pure b
67+
Just b
7168
end in
7269
match result
7370
when Just n -> n
@@ -78,11 +75,11 @@ let testNested =
7875
let safeDivide = x y -> if y == 0 then Nothing else Just (Int.div x y)
7976

8077
let testChain =
81-
let result = do
78+
let result = do[Maybe]
8279
a <- safeDivide 100 2
8380
b <- safeDivide a 5
8481
c <- safeDivide b 2
85-
pure c
82+
Just c
8683
end in
8784
match result
8885
when Just n -> n
@@ -91,11 +88,11 @@ let testChain =
9188

9289
-- Division by zero short-circuits
9390
let testDivByZero =
94-
let result = do
91+
let result = do[Maybe]
9592
a <- safeDivide 100 2
9693
b <- safeDivide a 0
9794
c <- safeDivide b 2
98-
pure c
95+
Just c
9996
end in
10097
match result
10198
when Just _ -> 0
@@ -111,11 +108,11 @@ let rec listNth = n xs ->
111108

112109
let testListLookup =
113110
let items = [10, 20, 30, 40, 50] in
114-
let result = do
111+
let result = do[Maybe]
115112
first <- listNth 0 items
116113
third <- listNth 2 items
117114
last <- listNth 4 items
118-
pure (first + third + last)
115+
Just (first + third + last)
119116
end in
120117
match result
121118
when Just n -> n

integration/json-decode/main.alg

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
-- =============================================================================
44
-- Demonstrates both combinator-style and do-notation for JSON parsing
55

6-
-- Bring Maybe's flatMap into scope for do-notation
7-
let flatMap = Maybe.flatMap
8-
let pure = x -> Just x
9-
106
-- Define a Todo type matching jsonplaceholder.typicode.com/todos schema
117
type Todo = Todo {
128
userId: int,
@@ -30,12 +26,12 @@ let decodeTodoCombinator = json ->
3026
-- Do-Notation Style (cleaner!)
3127
-- =============================================================================
3228

33-
let decodeTodoDo = json -> do
29+
let decodeTodoDo = json -> do[Maybe]
3430
userId <- Json.at "userId" Json.int json
3531
id <- Json.at "id" Json.int json
3632
title <- Json.at "title" Json.string json
3733
completed <- Json.at "completed" Json.bool json
38-
pure (Todo { userId = userId, id = id, title = title, completed = completed })
34+
Just (Todo { userId = userId, id = id, title = title, completed = completed })
3935
end
4036

4137
-- Decode a list of Todos

src/desugar.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ export const desugarExpr = (state: DesugarState, expr: S.SExpr): C.CExpr => {
284284

285285
case "SDo":
286286
// D⟦ SDo stmts ⟧ = D_do⟦stmts⟧
287-
return desugarDo(state, expr.stmts, expr.span);
287+
return desugarDo(state, expr.moduleName, expr.stmts, expr.span);
288288

289289
case "SAnnot":
290290
// D⟦ SAnnot e t ⟧ = D⟦e⟧
@@ -366,13 +366,21 @@ const desugarCase = (state: DesugarState, c: S.SCase): C.CCase => ({
366366
// Do-Notation Desugaring (Section 6.2)
367367
// =============================================================================
368368

369-
const desugarDo = (state: DesugarState, stmts: readonly S.SDoStmt[], span: S.Span): C.CExpr => {
369+
const desugarDo = (
370+
state: DesugarState,
371+
moduleName: string,
372+
stmts: readonly S.SDoStmt[],
373+
span: S.Span,
374+
): C.CExpr => {
370375
if (stmts.length === 0) {
371376
// Empty do block - should have been caught by parser
372377
// Synthetic node
373378
return C.clit(freshNodeId(state), { kind: "int", value: 0 }, span);
374379
}
375380

381+
// Use Module.flatMap from the specified module
382+
const flatMapName = `${moduleName}.flatMap`;
383+
376384
const lastStmt = stmts[stmts.length - 1]!;
377385

378386
// D_do⟦ [DoExpr e] ⟧ = D⟦e⟧
@@ -400,7 +408,7 @@ const desugarDo = (state: DesugarState, stmts: readonly S.SDoStmt[], span: S.Spa
400408
freshNodeId(state),
401409
C.capp(
402410
freshNodeId(state),
403-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
411+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
404412
C.cabs(
405413
freshNodeId(state),
406414
unresolvedName(state, stmt.pattern.name.text, stmt.pattern.name.span),
@@ -422,7 +430,7 @@ const desugarDo = (state: DesugarState, stmts: readonly S.SDoStmt[], span: S.Spa
422430
freshNodeId(state),
423431
C.capp(
424432
freshNodeId(state),
425-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
433+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
426434
C.cabs(
427435
freshNodeId(state),
428436
unresolvedName(state, tmp, stmtSpan),
@@ -474,7 +482,7 @@ const desugarDo = (state: DesugarState, stmts: readonly S.SDoStmt[], span: S.Spa
474482
freshNodeId(state),
475483
C.capp(
476484
freshNodeId(state),
477-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
485+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
478486
C.cabs(freshNodeId(state), unresolvedName(state, tmp, stmtSpan), result, stmtSpan),
479487
stmtSpan,
480488
),
@@ -1229,6 +1237,7 @@ const desugarExprInModuleWithImports = (
12291237
case "SDo":
12301238
return desugarDoInModuleWithImports(
12311239
state,
1240+
expr.moduleName,
12321241
expr.stmts,
12331242
moduleName,
12341243
moduleNames,
@@ -1328,6 +1337,7 @@ const desugarListInModuleWithImports = (
13281337

13291338
const desugarDoInModuleWithImports = (
13301339
state: DesugarState,
1340+
doModuleName: string,
13311341
stmts: readonly S.SDoStmt[],
13321342
moduleName: string,
13331343
moduleNames: Set<string>,
@@ -1342,6 +1352,9 @@ const desugarDoInModuleWithImports = (
13421352
return C.clit(freshNodeId(state), { kind: "int", value: 0 }, span);
13431353
}
13441354

1355+
// Use Module.flatMap from the specified module (from do[Module] syntax)
1356+
const flatMapName = `${doModuleName}.flatMap`;
1357+
13451358
const lastStmt = stmts[stmts.length - 1]!;
13461359

13471360
if (stmts.length === 1 && lastStmt.kind === "DoExpr") {
@@ -1365,7 +1378,7 @@ const desugarDoInModuleWithImports = (
13651378
freshNodeId(state),
13661379
C.capp(
13671380
freshNodeId(state),
1368-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
1381+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
13691382
C.cabs(
13701383
freshNodeId(state),
13711384
unresolvedName(state, stmt.pattern.name.text, stmt.pattern.name.span),
@@ -1383,7 +1396,7 @@ const desugarDoInModuleWithImports = (
13831396
freshNodeId(state),
13841397
C.capp(
13851398
freshNodeId(state),
1386-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
1399+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
13871400
C.cabs(
13881401
freshNodeId(state),
13891402
unresolvedName(state, tmp, stmtSpan),
@@ -1456,7 +1469,7 @@ const desugarDoInModuleWithImports = (
14561469
freshNodeId(state),
14571470
C.capp(
14581471
freshNodeId(state),
1459-
C.cvar(freshNodeId(state), unresolvedName(state, "flatMap", stmtSpan), stmtSpan),
1472+
C.cvar(freshNodeId(state), unresolvedName(state, flatMapName, stmtSpan), stmtSpan),
14601473
C.cabs(freshNodeId(state), unresolvedName(state, tmp, stmtSpan), result, stmtSpan),
14611474
stmtSpan,
14621475
),

src/parser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,12 @@ const parseDo = (state: ParserState): S.SDo => {
13221322
const start = state.current[1];
13231323
advance(state); // do
13241324

1325+
// Parse required module qualifier: do[Module] ... end
1326+
expect(state, TokenKind.LBracket, "expected '[' after 'do' - syntax is do[Module]");
1327+
const moduleToken = expect(state, TokenKind.Upper, "expected module name");
1328+
const moduleName = moduleToken ? text(state, moduleToken) : "";
1329+
expect(state, TokenKind.RBracket, "expected ']' after module name");
1330+
13251331
const stmts: S.SDoStmt[] = [];
13261332

13271333
while (!at(state, TokenKind.End) && !at(state, TokenKind.Eof)) {
@@ -1369,7 +1375,7 @@ const parseDo = (state: ParserState): S.SDo => {
13691375
const endToken = expect(state, TokenKind.End, "expected 'end'");
13701376
const end = endToken ? endToken[2] : state.current[1];
13711377

1372-
return S.sdo(nextNodeId(state), stmts, span(state, start, end));
1378+
return S.sdo(nextNodeId(state), moduleName, stmts, span(state, start, end));
13731379
};
13741380

13751381
const parseLetExpr = (state: ParserState): S.SExpr => {

src/surface.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,11 @@ export type SBinOp = {
237237
};
238238

239239
// Do-notation (Section 4.3)
240+
// Required module qualifier: do[Maybe] ... end desugars to Maybe.flatMap
240241
export type SDo = {
241242
readonly kind: "SDo";
242243
readonly nodeId: NodeId;
244+
readonly moduleName: string;
243245
readonly stmts: readonly SDoStmt[];
244246
readonly span: Span;
245247
};
@@ -660,9 +662,15 @@ export const sbinop = (
660662
right,
661663
span,
662664
});
663-
export const sdo = (nodeId: NodeId, stmts: readonly SDoStmt[], span: Span): SDo => ({
665+
export const sdo = (
666+
nodeId: NodeId,
667+
moduleName: string,
668+
stmts: readonly SDoStmt[],
669+
span: Span,
670+
): SDo => ({
664671
kind: "SDo",
665672
nodeId,
673+
moduleName,
666674
stmts,
667675
span,
668676
});

0 commit comments

Comments
 (0)