+2
.gitignore
+2
.gitignore
+23
Cargo.lock
+23
Cargo.lock
···
175
175
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
176
176
177
177
[[package]]
178
+
name = "equivalent"
179
+
version = "1.0.2"
180
+
source = "registry+https://github.com/rust-lang/crates.io-index"
181
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
182
+
183
+
[[package]]
178
184
name = "errno"
179
185
version = "0.3.14"
180
186
source = "registry+https://github.com/rust-lang/crates.io-index"
···
203
209
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
204
210
205
211
[[package]]
212
+
name = "hashbrown"
213
+
version = "0.16.0"
214
+
source = "registry+https://github.com/rust-lang/crates.io-index"
215
+
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
216
+
217
+
[[package]]
206
218
name = "heck"
207
219
version = "0.5.0"
208
220
source = "registry+https://github.com/rust-lang/crates.io-index"
209
221
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
222
+
223
+
[[package]]
224
+
name = "indexmap"
225
+
version = "2.11.4"
226
+
source = "registry+https://github.com/rust-lang/crates.io-index"
227
+
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
228
+
dependencies = [
229
+
"equivalent",
230
+
"hashbrown",
231
+
]
210
232
211
233
[[package]]
212
234
name = "is_ci"
···
540
562
source = "registry+https://github.com/rust-lang/crates.io-index"
541
563
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
542
564
dependencies = [
565
+
"indexmap",
543
566
"itoa",
544
567
"memchr",
545
568
"ryu",
+219
-170
SPEC.md
+219
-170
SPEC.md
···
24
24
The `#` character is reserved for shebangs only and is not used elsewhere in the syntax.
25
25
26
26
### File Naming Convention
27
-
Files should follow the lexicon NSID:
27
+
The file path determines the lexicon NSID. Files should follow the lexicon NSID structure:
28
28
- `app.bsky.feed.post.mlf` → Lexicon NSID: `app.bsky.feed.post`
29
29
- `sh.tangled.repo.issue.mlf` → Lexicon NSID: `sh.tangled.repo.issue`
30
+
31
+
The lexicon NSID is derived solely from the filename, not from any internal namespace declarations.
30
32
31
33
## Core Concepts
32
34
···
50
52
1. **Local (same file)**: Just use the name
51
53
```mlf
52
54
record myRecord {
53
-
field: myAlias, // References alias in same file
55
+
field: myType // References type in same file
54
56
}
55
57
56
-
alias myAlias = { /* ... */ };
58
+
def type myType = { /* ... */ }
57
59
```
58
60
59
61
2. **Cross-file (different lexicon)**: Use full dotted path
60
62
```mlf
61
63
record myRecord {
62
-
profile: app.bsky.actor.profile, // References app/bsky/actor/profile.mlf
63
-
author: com.example.user.author, // References com/example/user/author.mlf
64
+
profile: app.bsky.actor.profile // References app/bsky/actor/profile.mlf
65
+
author: com.example.user.author // References com/example/user/author.mlf
64
66
}
65
67
```
66
68
67
69
**Note**: The `#` character is NOT used for references. All references use dotted notation.
68
70
71
+
### Syntax Rules
72
+
73
+
#### Semicolons
74
+
75
+
- **Records** do NOT have semicolons after the closing brace `}`
76
+
- All other definitions require semicolons:
77
+
- `use` statements end with `;`
78
+
- `token` definitions end with `;`
79
+
- `inline type` definitions end with `;`
80
+
- `def type` definitions end with `;`
81
+
- `query` definitions end with `;`
82
+
- `procedure` definitions end with `;`
83
+
- `subscription` definitions end with `;`
84
+
85
+
#### Commas
86
+
87
+
Commas are **required** between items, with **trailing commas allowed**:
88
+
89
+
- **Record fields**: Commas required between fields, trailing comma allowed
90
+
```mlf
91
+
record example {
92
+
field1: string,
93
+
field2: integer, // trailing comma allowed
94
+
}
95
+
```
96
+
97
+
- **Constraints**: Commas required between constraint properties, trailing comma allowed
98
+
```mlf
99
+
title: string constrained {
100
+
maxLength: 200,
101
+
minLength: 1, // trailing comma allowed
102
+
}
103
+
```
104
+
105
+
- **Error definitions**: Commas required between errors, trailing comma allowed
106
+
```mlf
107
+
query getThread(): thread | error {
108
+
NotFound,
109
+
BadRequest, // trailing comma allowed
110
+
}
111
+
```
112
+
69
113
## Type System
70
114
71
115
### Primitive Types
···
106
150
With constraints:
107
151
```mlf
108
152
avatar: blob constrained {
109
-
accept: ["image/png", "image/jpeg"],
110
-
maxSize: 1000000, // bytes
153
+
accept: ["image/png", "image/jpeg"]
154
+
maxSize: 1000000 // bytes
111
155
}
112
156
```
113
157
···
126
170
```mlf
127
171
record post {
128
172
text: string constrained {
129
-
maxLength: 300,
130
-
maxGraphemes: 300,
131
-
},
132
-
createdAt: Datetime,
133
-
reply?: replyRef, // Optional field
173
+
maxLength: 300
174
+
maxGraphemes: 300
175
+
}
176
+
createdAt: Datetime
177
+
reply?: replyRef // Optional field
134
178
}
135
179
```
136
180
137
-
### Aliases
181
+
### Type Definitions
182
+
183
+
MLF supports two kinds of type definitions:
184
+
185
+
**Inline Types** - Expanded at the point of use, never appear in generated lexicon defs:
186
+
187
+
```mlf
188
+
inline type AtIdentifier = string constrained {
189
+
format "at-identifier"
190
+
};
191
+
```
138
192
139
-
Type aliases define reusable object shapes:
193
+
**Def Types** - Become named definitions in the lexicon's defs block:
140
194
141
195
```mlf
142
-
alias replyRef = {
143
-
root: AtUri,
144
-
parent: AtUri,
196
+
def type ReplyRef = {
197
+
root: AtUri
198
+
parent: AtUri
145
199
};
146
200
```
147
201
148
-
If used in multiple places, they will be hoisted to a def. If only used in a single place, they will be inlined.
202
+
Use `inline type` for type aliases that should be expanded inline (like primitive type wrappers). Use `def type` for types that should be referenced by name in the generated lexicon.
149
203
150
204
### Tokens
151
205
···
161
215
record issue {
162
216
state: string constrained {
163
217
knownValues: [
164
-
open, // References token defined above
165
-
closed,
166
-
],
167
-
default: "open",
168
-
},
218
+
open // References token defined above
219
+
closed
220
+
]
221
+
default: "open"
222
+
}
169
223
}
170
224
```
171
225
···
179
233
/// Get a user profile
180
234
query getProfile(
181
235
/// The actor's DID or handle
182
-
actor: AtIdentifier,
236
+
actor: AtIdentifier
183
237
/// Optional viewer context
184
-
viewer?: Did,
238
+
viewer?: Did
185
239
): profileView | error {
186
240
/// Profile not found
187
-
ProfileNotFound,
241
+
ProfileNotFound
188
242
/// Invalid request parameters
189
-
BadRequest,
243
+
BadRequest
190
244
};
191
245
```
192
246
···
197
251
```mlf
198
252
/// Create a new post
199
253
procedure createPost(
200
-
text: string,
201
-
createdAt: Datetime,
254
+
text: string
255
+
createdAt: Datetime
202
256
): {
203
-
uri: AtUri,
204
-
cid: Cid,
257
+
uri: AtUri
258
+
cid: Cid
205
259
} | error {
206
260
/// Text exceeds maximum length
207
-
TextTooLong,
261
+
TextTooLong
208
262
};
209
263
```
210
264
···
216
270
/// Subscribe to repository events
217
271
subscription subscribeRepos(
218
272
/// Optional cursor for resuming from a specific point
219
-
cursor?: integer,
273
+
cursor?: integer
220
274
): commit | identity | handle | migrate | tombstone | info;
221
275
```
222
276
223
-
**Message definitions** for subscriptions are defined as aliases or records:
277
+
**Message definitions** for subscriptions are defined as def types or records:
224
278
225
279
```mlf
226
280
/// Commit message emitted by subscribeRepos
227
-
alias commit = {
228
-
seq: integer,
229
-
rebase: boolean,
230
-
tooBig: boolean,
231
-
repo: Did,
232
-
commit: Cid,
233
-
rev: string,
234
-
since: string,
235
-
blocks: bytes,
236
-
ops: repoOp[],
237
-
blobs: Cid[],
238
-
time: Datetime,
281
+
def type commit = {
282
+
seq: integer
283
+
rebase: boolean
284
+
tooBig: boolean
285
+
repo: Did
286
+
commit: Cid
287
+
rev: string
288
+
since: string
289
+
blocks: bytes
290
+
ops: repoOp[]
291
+
blobs: Cid[]
292
+
time: Datetime
239
293
};
240
294
241
295
/// Info message
242
-
alias info = {
243
-
name: string,
244
-
message?: string,
296
+
def type info = {
297
+
name: string
298
+
message?: string
245
299
};
246
300
```
247
301
···
249
303
250
304
- Parameters: Like queries, subscriptions can have parameters
251
305
- Return type: A union of message types that can be emitted
252
-
- Each message type must be defined as an alias or record
306
+
- Each message type must be defined as a def type or record
253
307
- Message types can be local or imported from other lexicons
254
308
- Subscriptions are long-lived WebSocket connections
255
309
- No error block (errors are handled at the WebSocket protocol level)
···
260
314
/// Subscribe to chat messages for a stream
261
315
subscription subscribeChat(
262
316
/// The DID of the streamer
263
-
streamer: Did,
317
+
streamer: Did
264
318
/// Optional cursor to resume from
265
-
cursor?: string,
319
+
cursor?: string
266
320
): message | delete | join | leave;
267
321
268
322
/// Chat message payload
269
-
alias message = {
270
-
id: string,
271
-
text: string,
272
-
author: Did,
273
-
createdAt: Datetime,
323
+
def type message = {
324
+
id: string
325
+
text: string
326
+
author: Did
327
+
createdAt: Datetime
274
328
};
275
329
276
330
/// Delete event payload
277
-
alias delete = {
278
-
id: string,
331
+
def type delete = {
332
+
id: string
279
333
};
280
334
281
335
/// Join event payload
282
-
alias join = {
283
-
user: Did,
336
+
def type join = {
337
+
user: Did
284
338
};
285
339
286
340
/// Leave event payload
287
-
alias leave = {
288
-
user: Did,
341
+
def type leave = {
342
+
user: Did
289
343
};
290
344
```
291
345
···
304
358
305
359
```mlf
306
360
record example {
307
-
required: string,
308
-
optional?: string,
361
+
required: string
362
+
optional?: string
309
363
}
310
364
```
311
365
···
313
367
314
368
```mlf
315
369
record example {
316
-
tags: string[],
370
+
tags: string[]
317
371
items: string[] constrained {
318
-
minLength: 1,
319
-
maxLength: 10,
320
-
},
372
+
minLength: 1
373
+
maxLength: 10
374
+
}
321
375
}
322
376
```
323
377
···
328
382
```mlf
329
383
record example {
330
384
// Closed union (only these types)
331
-
content: text | image | video,
385
+
content: text | image | video
332
386
333
387
// Union of tokens
334
-
state: open | closed | pending,
388
+
state: open | closed | pending
335
389
}
336
390
```
337
391
···
340
394
```mlf
341
395
record example {
342
396
// Open union (can include unknown types)
343
-
content: text | image | _,
397
+
content: text | image | _
344
398
}
345
399
```
346
400
···
351
405
```mlf
352
406
// Local reference (same file)
353
407
record post {
354
-
author: author, // References 'alias author' in same file
408
+
author: author // References 'def type author' in same file
355
409
}
356
410
357
411
// Cross-file reference
358
412
record post {
359
-
profile: app.bsky.actor.profile, // References app/bsky/actor/profile.mlf
413
+
profile: app.bsky.actor.profile // References app/bsky/actor/profile.mlf
360
414
}
361
415
```
362
416
···
370
424
371
425
```mlf
372
426
// Valid: More restrictive constraints
373
-
alias shortString = string constrained {
374
-
maxLength: 100,
427
+
def type shortString = string constrained {
428
+
maxLength: 100
375
429
};
376
430
377
431
record post {
378
432
// Can further constrain to 50 (more restrictive than 100)
379
433
title: shortString constrained {
380
-
maxLength: 50, // ✓ Valid: 50 ≤ 100
381
-
},
434
+
maxLength: 50 // ✓ Valid: 50 ≤ 100
435
+
}
382
436
}
383
437
384
438
// Invalid: Less restrictive constraints
385
439
record invalid {
386
440
// ERROR: Cannot expand to 200 (less restrictive than 100)
387
441
content: shortString constrained {
388
-
maxLength: 200, // ✗ Invalid: 200 > 100
389
-
},
442
+
maxLength: 200 // ✗ Invalid: 200 > 100
443
+
}
390
444
}
391
445
```
392
446
···
403
457
404
458
```mlf
405
459
field: string constrained {
406
-
minLength: 1, // Minimum byte length
407
-
maxLength: 1000, // Maximum byte length
408
-
minGraphemes: 1, // Minimum grapheme clusters
409
-
maxGraphemes: 100, // Maximum grapheme clusters
410
-
format: "uri", // Format validation
411
-
enum: ["a", "b", "c"], // Allowed values (closed set)
412
-
knownValues: [ // Known values (extensible set)
413
-
value1,
414
-
value2,
415
-
],
416
-
default: "defaultValue", // Default value
460
+
minLength: 1 // Minimum byte length
461
+
maxLength: 1000 // Maximum byte length
462
+
minGraphemes: 1 // Minimum grapheme clusters
463
+
maxGraphemes: 100 // Maximum grapheme clusters
464
+
format: "uri" // Format validation
465
+
enum: ["a", "b", "c"] // Allowed values (closed set) - string literals
466
+
knownValues: [ // Known values (extensible set) - can be string literals OR token references
467
+
value1 // Token reference
468
+
"value2" // String literal
469
+
]
470
+
default: "defaultValue" // Default value
417
471
}
418
472
```
419
473
474
+
**Note**: `enum`, `knownValues`, and `default` can accept either:
475
+
- **Literals**: `"open"`, `42`, `true` (string, integer, or boolean)
476
+
- **References**: `open`, `myType` (references to tokens, records, types, etc.)
477
+
478
+
When using references, the identifier will be resolved to its string representation in the generated lexicon.
479
+
420
480
### Integer Constraints
421
481
422
482
```mlf
423
483
field: integer constrained {
424
-
minimum: 0,
425
-
maximum: 100,
426
-
enum: [1, 2, 3],
427
-
default: 1,
484
+
minimum: 0
485
+
maximum: 100
486
+
enum: [1, 2, 3]
487
+
default: 1
428
488
}
429
489
```
430
490
···
432
492
433
493
```mlf
434
494
field: string[] constrained {
435
-
minLength: 1,
436
-
maxLength: 10,
495
+
minLength: 1
496
+
maxLength: 10
437
497
}
438
498
```
439
499
···
441
501
442
502
```mlf
443
503
field: blob constrained {
444
-
accept: ["image/png", "image/jpeg"], // MIME types
445
-
maxSize: 1000000, // Bytes
504
+
accept: ["image/png", "image/jpeg"] // MIME types
505
+
maxSize: 1000000 // Bytes
446
506
}
447
507
```
448
508
···
450
510
451
511
```mlf
452
512
field: boolean constrained {
453
-
default: false,
513
+
default: false
454
514
}
455
515
```
456
516
···
464
524
/// A user profile record
465
525
record profile {
466
526
/// The user's display name
467
-
displayName?: string,
527
+
displayName?: string
468
528
}
469
529
```
470
530
···
484
544
```mlf
485
545
@deprecated
486
546
record oldRecord {
487
-
field: string,
547
+
field: string
488
548
}
489
549
```
490
550
···
493
553
@since(1, 2, 0)
494
554
@doc("https://example.com/docs")
495
555
record example {
496
-
field: string,
556
+
field: string
497
557
}
498
558
```
499
559
···
507
567
@validate(min: 0, max: 100, strict: true)
508
568
@codegen(language: "rust", derive: "Debug, Clone")
509
569
record example {
510
-
field: integer,
570
+
field: integer
511
571
}
512
572
```
513
573
···
515
575
516
576
Annotations can be placed on:
517
577
- Records
518
-
- Aliases
578
+
- Inline Types
579
+
- Def Types
519
580
- Tokens
520
581
- Queries
521
582
- Procedures
522
583
- Subscriptions
523
-
- Fields within records/aliases
584
+
- Fields within records/types
524
585
525
586
```mlf
526
587
/// A user profile
···
528
589
record profile {
529
590
/// User's DID
530
591
@indexed
531
-
did: Did,
592
+
did: Did
532
593
533
594
/// Display name
534
595
@sensitive(pii: true)
535
-
displayName?: string,
596
+
displayName?: string
536
597
}
537
598
```
538
599
···
567
628
568
629
**Note:** The interpretation of annotations is entirely up to the tooling consuming the MLF. Different tools may support different annotation sets.
569
630
570
-
## Namespaces
571
-
572
-
Organize related definitions within namespaces:
573
-
574
-
```mlf
575
-
namespace .actor {
576
-
record profile {
577
-
displayName?: string,
578
-
}
579
-
580
-
query getProfile(
581
-
actor: AtIdentifier,
582
-
): profile;
583
-
}
584
-
585
-
namespace .feed {
586
-
record post {
587
-
text: string,
588
-
}
589
-
}
590
-
```
591
-
592
631
## Use Statements
593
632
594
633
Import definitions from other lexicons:
···
614
653
use app.bsky.actor.profile;
615
654
616
655
record myThing {
617
-
author: profile, // Instead of app.bsky.actor.profile
656
+
author: profile // Instead of app.bsky.actor.profile
618
657
}
619
658
```
620
659
···
641
680
642
681
### File Path Convention
643
682
644
-
Lexicons follow a directory structure matching their NSID:
683
+
The lexicon NSID is determined by the file path. Lexicons can follow a directory structure matching their NSID:
645
684
646
685
```
647
686
lexicons/
···
656
695
thing.mlf → com.example.thing
657
696
```
658
697
659
-
Or flat with dots in filename:
698
+
Or use a flat structure with dots in the filename:
660
699
```
661
700
lexicons/
662
701
app.bsky.actor.profile.mlf
···
664
703
com.example.thing.mlf
665
704
```
666
705
706
+
In both cases, the NSID is derived from the file path, not from internal declarations.
707
+
667
708
## CLI Commands
668
709
669
710
```bash
···
701
742
/// An issue in a repository
702
743
record issue {
703
744
/// The repository this issue belongs to
704
-
repo: AtUri,
745
+
repo: AtUri
705
746
/// Issue title
706
747
title: string constrained {
707
-
minGraphemes: 1,
708
-
maxGraphemes: 200,
709
-
},
748
+
minGraphemes: 1
749
+
maxGraphemes: 200
750
+
}
710
751
/// Issue body (markdown)
711
752
body?: string constrained {
712
-
maxGraphemes: 10000,
713
-
},
753
+
maxGraphemes: 10000
754
+
}
714
755
/// Issue state
715
756
state: string constrained {
716
757
knownValues: [
717
-
open,
718
-
closed,
719
-
],
720
-
default: "open",
721
-
},
758
+
open
759
+
closed
760
+
]
761
+
default: "open"
762
+
}
722
763
/// Creation timestamp
723
-
createdAt: Datetime,
764
+
createdAt: Datetime
724
765
}
725
766
726
767
/// A comment on an issue
727
768
record comment {
728
769
/// The issue this comment belongs to
729
-
issue: AtUri,
770
+
issue: AtUri
730
771
/// Comment body (markdown)
731
772
body: string constrained {
732
-
minGraphemes: 1,
733
-
maxGraphemes: 10000,
734
-
},
773
+
minGraphemes: 1
774
+
maxGraphemes: 10000
775
+
}
735
776
/// Creation timestamp
736
-
createdAt: Datetime,
777
+
createdAt: Datetime
737
778
/// Optional reply target
738
-
replyTo?: AtUri,
779
+
replyTo?: AtUri
739
780
}
740
781
741
782
/// Get an issue by URI
742
783
query getIssue(
743
784
/// Issue AT-URI
744
-
uri: AtUri,
785
+
uri: AtUri
745
786
): issue | error {
746
787
/// Issue not found
747
-
NotFound,
788
+
NotFound
748
789
};
749
790
750
791
/// Create a new issue
751
792
procedure createIssue(
752
-
repo: AtUri,
753
-
title: string,
754
-
body?: string,
793
+
repo: AtUri
794
+
title: string
795
+
body?: string
755
796
): {
756
-
uri: AtUri,
757
-
cid: Cid,
797
+
uri: AtUri
798
+
cid: Cid
758
799
} | error {
759
800
/// Repository not found
760
-
RepoNotFound,
801
+
RepoNotFound
761
802
/// Title too long
762
-
TitleTooLong,
803
+
TitleTooLong
763
804
};
764
805
```
765
806
···
773
814
```mlf
774
815
record post {
775
816
text: string constrained {
776
-
maxLength: 300,
777
-
},
778
-
createdAt: Datetime,
817
+
maxLength: 300
818
+
}
819
+
createdAt: Datetime
779
820
}
780
821
```
781
822
···
812
853
**MLF:**
813
854
```mlf
814
855
subscription subscribeRepos(
815
-
cursor?: integer,
856
+
cursor?: integer
816
857
): commit | identity;
817
858
```
818
859
···
872
913
### Reserved Keywords
873
914
874
915
```
875
-
alias, as, blob, boolean, bytes, constrained, error, integer,
876
-
namespace, null, number, procedure, query, record, string,
877
-
subscription, token, unknown, use
916
+
as, blob, boolean, bytes, constrained, def, error, inline, integer,
917
+
null, number, procedure, query, record, string, subscription, token,
918
+
type, unknown, use
919
+
```
920
+
921
+
### Reserved Names
922
+
923
+
The following names cannot be used as item names:
924
+
925
+
```
926
+
main, defs
878
927
```
879
928
880
929
### Raw Identifiers
···
882
931
To use a reserved keyword as an identifier, wrap it in backticks:
883
932
884
933
```mlf
885
-
alias `record` = {
886
-
`record`: com.atproto.repo.strongRef,
887
-
`error`: string,
934
+
def type `record` = {
935
+
`record`: com.atproto.repo.strongRef
936
+
`error`: string
888
937
};
889
938
```
890
939
+25
-14
mlf-cli/src/generate/lexicon.rs
+25
-14
mlf-cli/src/generate/lexicon.rs
···
86
86
}
87
87
};
88
88
89
-
let namespace = extract_namespace(&file_path, &lexicon);
89
+
let namespace = extract_namespace(&file_path);
90
+
91
+
// Create workspace with prelude for inline type resolution
92
+
let mut workspace = match mlf_lang::Workspace::with_prelude() {
93
+
Ok(ws) => ws,
94
+
Err(e) => {
95
+
errors.push((file_path.display().to_string(), format!("Failed to load prelude: {:?}", e)));
96
+
continue;
97
+
}
98
+
};
99
+
100
+
// Add the module to the workspace
101
+
if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) {
102
+
errors.push((file_path.display().to_string(), format!("Failed to add module: {:?}", e)));
103
+
continue;
104
+
}
90
105
91
-
let json_lexicon = mlf_codegen::generate_lexicon(&namespace, &lexicon);
106
+
// Resolve types
107
+
if let Err(e) = workspace.resolve() {
108
+
errors.push((file_path.display().to_string(), format!("Type resolution error: {:?}", e)));
109
+
continue;
110
+
}
111
+
112
+
let json_lexicon = mlf_codegen::generate_lexicon(&namespace, &lexicon, &workspace);
92
113
93
114
let output_path = if flat {
94
115
output_dir.join(format!("{}.json", namespace))
···
131
152
Ok(())
132
153
}
133
154
134
-
fn extract_namespace(file_path: &Path, lexicon: &mlf_lang::ast::Lexicon) -> String {
135
-
use mlf_lang::ast::Item;
136
-
137
-
for item in &lexicon.items {
138
-
if let Item::Namespace(ns) = item {
139
-
if ns.name.name.starts_with('.') {
140
-
continue;
141
-
}
142
-
return ns.name.name.clone();
143
-
}
144
-
}
145
-
155
+
fn extract_namespace(file_path: &Path) -> String {
156
+
// Namespace is derived solely from the filename
146
157
file_path
147
158
.file_stem()
148
159
.and_then(|s| s.to_str())
+1
-1
mlf-codegen/Cargo.toml
+1
-1
mlf-codegen/Cargo.toml
+188
-110
mlf-codegen/src/lib.rs
+188
-110
mlf-codegen/src/lib.rs
···
1
1
use mlf_lang::ast::*;
2
+
use mlf_lang::Workspace;
2
3
use serde_json::{json, Map, Value};
3
4
use std::collections::HashMap;
4
5
5
-
pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon) -> Value {
6
+
pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon, workspace: &Workspace) -> Value {
6
7
let usage_counts = analyze_type_usage(lexicon);
7
8
9
+
// Extract the last segment of the namespace to determine main
10
+
let namespace_parts: Vec<&str> = namespace.split('.').collect();
11
+
let expected_main_name = namespace_parts.last().copied().unwrap_or("");
12
+
let is_defs_namespace = expected_main_name == "defs";
13
+
14
+
// Count main-eligible items (records, queries, procedures, subscriptions)
15
+
let main_eligible_count = lexicon.items.iter().filter(|item| {
16
+
matches!(item, Item::Record(_) | Item::Query(_) | Item::Procedure(_) | Item::Subscription(_))
17
+
}).count();
18
+
8
19
let mut defs = Map::new();
9
-
let mut main_def: Option<Value> = None;
10
20
11
21
for item in &lexicon.items {
12
22
match item {
13
23
Item::Record(record) => {
14
-
let record_json = generate_record_json(record, &usage_counts);
15
-
main_def = Some(record_json);
24
+
let record_json = generate_record_json(record, &usage_counts, workspace, namespace);
25
+
// If there's only one main-eligible item, it becomes "main"
26
+
if main_eligible_count == 1 || (!is_defs_namespace && record.name.name == expected_main_name) {
27
+
defs.insert("main".to_string(), record_json);
28
+
} else {
29
+
defs.insert(record.name.name.clone(), record_json);
30
+
}
16
31
}
17
32
Item::Query(query) => {
18
-
let query_json = generate_query_json(query, &usage_counts);
19
-
main_def = Some(query_json);
33
+
let query_json = generate_query_json(query, &usage_counts, workspace, namespace);
34
+
if main_eligible_count == 1 || (!is_defs_namespace && query.name.name == expected_main_name) {
35
+
defs.insert("main".to_string(), query_json);
36
+
} else {
37
+
defs.insert(query.name.name.clone(), query_json);
38
+
}
20
39
}
21
40
Item::Procedure(procedure) => {
22
-
let procedure_json = generate_procedure_json(procedure, &usage_counts);
23
-
main_def = Some(procedure_json);
41
+
let procedure_json = generate_procedure_json(procedure, &usage_counts, workspace, namespace);
42
+
if main_eligible_count == 1 || (!is_defs_namespace && procedure.name.name == expected_main_name) {
43
+
defs.insert("main".to_string(), procedure_json);
44
+
} else {
45
+
defs.insert(procedure.name.name.clone(), procedure_json);
46
+
}
24
47
}
25
48
Item::Subscription(subscription) => {
26
-
let subscription_json = generate_subscription_json(subscription, &usage_counts);
27
-
main_def = Some(subscription_json);
49
+
let subscription_json = generate_subscription_json(subscription, &usage_counts, workspace, namespace);
50
+
if main_eligible_count == 1 || (!is_defs_namespace && subscription.name.name == expected_main_name) {
51
+
defs.insert("main".to_string(), subscription_json);
52
+
} else {
53
+
defs.insert(subscription.name.name.clone(), subscription_json);
54
+
}
28
55
}
29
-
Item::Alias(alias) => {
30
-
if should_hoist_alias(&alias.name.name, &usage_counts) {
31
-
let alias_json = generate_alias_json(alias, &usage_counts);
32
-
defs.insert(alias.name.name.clone(), alias_json);
33
-
}
56
+
Item::DefType(def_type) => {
57
+
let def_type_json = generate_def_type_json(def_type, &usage_counts, workspace, namespace);
58
+
defs.insert(def_type.name.name.clone(), def_type_json);
59
+
}
60
+
Item::InlineType(_) => {
61
+
// Inline types are never added to defs - they expand at point of use
62
+
// TODO: inline expansion will be handled by workspace/cross-file resolution
34
63
}
35
64
Item::Token(token) => {
36
65
let token_json = json!({
···
43
72
}
44
73
}
45
74
46
-
if let Some(main) = main_def {
47
-
defs.insert("main".to_string(), main);
48
-
}
49
-
50
-
json!({
51
-
"lexicon": 1,
52
-
"id": namespace,
53
-
"defs": defs
54
-
})
75
+
let mut root = Map::new();
76
+
root.insert("lexicon".to_string(), json!(1));
77
+
root.insert("id".to_string(), json!(namespace));
78
+
root.insert("defs".to_string(), json!(defs));
79
+
Value::Object(root)
55
80
}
56
81
57
82
fn analyze_type_usage(lexicon: &Lexicon) -> HashMap<String, usize> {
···
92
117
}
93
118
count_type_references(&subscription.messages, &mut usage_counts);
94
119
}
95
-
Item::Alias(alias) => {
96
-
count_type_references(&alias.ty, &mut usage_counts);
120
+
Item::InlineType(inline_type) => {
121
+
count_type_references(&inline_type.ty, &mut usage_counts);
122
+
}
123
+
Item::DefType(def_type) => {
124
+
count_type_references(&def_type.ty, &mut usage_counts);
97
125
}
98
126
_ => {}
99
127
}
···
121
149
count_type_references(&field.ty, counts);
122
150
}
123
151
}
152
+
Type::Parenthesized { inner, .. } => count_type_references(inner, counts),
124
153
Type::Constrained { base, .. } => count_type_references(base, counts),
125
154
_ => {}
126
155
}
127
-
}
128
-
129
-
fn should_hoist_alias(name: &str, usage_counts: &HashMap<String, usize>) -> bool {
130
-
usage_counts.get(name).map_or(false, |&count| count > 1)
131
156
}
132
157
133
158
fn extract_docs(docs: &[DocComment]) -> String {
···
137
162
.join("\n")
138
163
}
139
164
140
-
fn generate_record_json(record: &Record, usage_counts: &HashMap<String, usize>) -> Value {
165
+
fn generate_record_json(record: &Record, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
141
166
let mut required = Vec::new();
142
167
let mut properties = Map::new();
143
168
···
146
171
required.push(field.name.name.clone());
147
172
}
148
173
149
-
let field_json = generate_type_json(&field.ty, usage_counts);
174
+
let field_json = generate_type_json(&field.ty, usage_counts, workspace, current_namespace);
150
175
properties.insert(field.name.name.clone(), field_json);
151
176
}
152
177
···
164
189
})
165
190
}
166
191
167
-
fn generate_query_json(query: &Query, usage_counts: &HashMap<String, usize>) -> Value {
192
+
fn generate_query_json(query: &Query, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
168
193
let mut params_properties = Map::new();
169
194
let mut params_required = Vec::new();
170
195
···
172
197
if !param.optional {
173
198
params_required.push(param.name.name.clone());
174
199
}
175
-
let param_json = generate_type_json(¶m.ty, usage_counts);
200
+
let mut param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
201
+
// Add description if the parameter has doc comments
202
+
if !param.docs.is_empty() {
203
+
if let Some(obj) = param_json.as_object_mut() {
204
+
obj.insert("description".to_string(), json!(extract_docs(¶m.docs)));
205
+
}
206
+
}
176
207
params_properties.insert(param.name.name.clone(), param_json);
177
208
}
178
209
179
210
let params = if !params_properties.is_empty() {
180
-
json!({
181
-
"type": "params",
182
-
"required": params_required,
183
-
"properties": params_properties
184
-
})
211
+
let mut params_obj = Map::new();
212
+
params_obj.insert("type".to_string(), json!("params"));
213
+
params_obj.insert("required".to_string(), json!(params_required));
214
+
params_obj.insert("properties".to_string(), json!(params_properties));
215
+
Value::Object(params_obj)
185
216
} else {
186
-
json!({
187
-
"type": "params",
188
-
"properties": {}
189
-
})
217
+
let mut params_obj = Map::new();
218
+
params_obj.insert("type".to_string(), json!("params"));
219
+
params_obj.insert("properties".to_string(), json!({}));
220
+
Value::Object(params_obj)
190
221
};
191
222
192
223
let output = match &query.returns {
193
224
ReturnType::Type(ty) => {
194
-
json!({
195
-
"encoding": "application/json",
196
-
"schema": generate_type_json(ty, usage_counts)
197
-
})
225
+
let mut output_obj = Map::new();
226
+
output_obj.insert("encoding".to_string(), json!("application/json"));
227
+
output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace));
228
+
Value::Object(output_obj)
198
229
}
199
230
ReturnType::TypeWithErrors { success, errors, .. } => {
200
231
let mut error_defs = Map::new();
···
207
238
);
208
239
}
209
240
210
-
json!({
211
-
"encoding": "application/json",
212
-
"schema": generate_type_json(success, usage_counts),
213
-
"errors": error_defs
214
-
})
241
+
let mut output_obj = Map::new();
242
+
output_obj.insert("encoding".to_string(), json!("application/json"));
243
+
output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace));
244
+
output_obj.insert("errors".to_string(), json!(error_defs));
245
+
Value::Object(output_obj)
215
246
}
216
247
};
217
248
218
-
json!({
219
-
"type": "query",
220
-
"description": extract_docs(&query.docs),
221
-
"parameters": params,
222
-
"output": output
223
-
})
249
+
let mut query_obj = Map::new();
250
+
query_obj.insert("type".to_string(), json!("query"));
251
+
query_obj.insert("description".to_string(), json!(extract_docs(&query.docs)));
252
+
query_obj.insert("parameters".to_string(), params);
253
+
query_obj.insert("output".to_string(), output);
254
+
Value::Object(query_obj)
224
255
}
225
256
226
-
fn generate_procedure_json(procedure: &Procedure, usage_counts: &HashMap<String, usize>) -> Value {
257
+
fn generate_procedure_json(procedure: &Procedure, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
227
258
let mut params_properties = Map::new();
228
259
let mut params_required = Vec::new();
229
260
···
231
262
if !param.optional {
232
263
params_required.push(param.name.name.clone());
233
264
}
234
-
let param_json = generate_type_json(¶m.ty, usage_counts);
265
+
let param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
235
266
params_properties.insert(param.name.name.clone(), param_json);
236
267
}
237
268
238
269
let input = if !params_properties.is_empty() {
239
-
json!({
240
-
"encoding": "application/json",
241
-
"schema": {
242
-
"type": "object",
243
-
"required": params_required,
244
-
"properties": params_properties
245
-
}
246
-
})
270
+
let mut schema_obj = Map::new();
271
+
schema_obj.insert("type".to_string(), json!("object"));
272
+
schema_obj.insert("required".to_string(), json!(params_required));
273
+
schema_obj.insert("properties".to_string(), json!(params_properties));
274
+
275
+
let mut input_obj = Map::new();
276
+
input_obj.insert("encoding".to_string(), json!("application/json"));
277
+
input_obj.insert("schema".to_string(), Value::Object(schema_obj));
278
+
Some(Value::Object(input_obj))
247
279
} else {
248
-
Value::Null
280
+
None
249
281
};
250
282
251
283
let output = match &procedure.returns {
252
284
ReturnType::Type(ty) => {
253
-
json!({
254
-
"encoding": "application/json",
255
-
"schema": generate_type_json(ty, usage_counts)
256
-
})
285
+
let mut output_obj = Map::new();
286
+
output_obj.insert("encoding".to_string(), json!("application/json"));
287
+
output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace));
288
+
Value::Object(output_obj)
257
289
}
258
290
ReturnType::TypeWithErrors { success, errors, .. } => {
259
291
let mut error_defs = Map::new();
···
266
298
);
267
299
}
268
300
269
-
json!({
270
-
"encoding": "application/json",
271
-
"schema": generate_type_json(success, usage_counts),
272
-
"errors": error_defs
273
-
})
301
+
let mut output_obj = Map::new();
302
+
output_obj.insert("encoding".to_string(), json!("application/json"));
303
+
output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace));
304
+
output_obj.insert("errors".to_string(), json!(error_defs));
305
+
Value::Object(output_obj)
274
306
}
275
307
};
276
308
277
-
let mut result = json!({
278
-
"type": "procedure",
279
-
"description": extract_docs(&procedure.docs),
280
-
"output": output
281
-
});
282
-
283
-
if !input.is_null() {
284
-
result["input"] = input;
309
+
let mut result = Map::new();
310
+
result.insert("type".to_string(), json!("procedure"));
311
+
result.insert("description".to_string(), json!(extract_docs(&procedure.docs)));
312
+
if let Some(input_val) = input {
313
+
result.insert("input".to_string(), input_val);
285
314
}
286
-
287
-
result
315
+
result.insert("output".to_string(), output);
316
+
Value::Object(result)
288
317
}
289
318
290
319
fn generate_subscription_json(
291
320
subscription: &Subscription,
292
321
usage_counts: &HashMap<String, usize>,
322
+
workspace: &Workspace,
323
+
current_namespace: &str,
293
324
) -> Value {
294
325
let mut params_properties = Map::new();
295
326
let mut params_required = Vec::new();
···
298
329
if !param.optional {
299
330
params_required.push(param.name.name.clone());
300
331
}
301
-
let param_json = generate_type_json(¶m.ty, usage_counts);
332
+
let param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
302
333
params_properties.insert(param.name.name.clone(), param_json);
303
334
}
304
335
···
313
344
};
314
345
315
346
let message = json!({
316
-
"schema": generate_type_json(&subscription.messages, usage_counts)
347
+
"schema": generate_type_json(&subscription.messages, usage_counts, workspace, current_namespace)
317
348
});
318
349
319
350
let mut result = json!({
···
329
360
result
330
361
}
331
362
332
-
fn generate_alias_json(alias: &Alias, usage_counts: &HashMap<String, usize>) -> Value {
333
-
generate_type_json(&alias.ty, usage_counts)
363
+
fn generate_def_type_json(def_type: &DefType, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
364
+
generate_type_json(&def_type.ty, usage_counts, workspace, current_namespace)
334
365
}
335
366
336
-
fn generate_type_json(ty: &Type, usage_counts: &HashMap<String, usize>) -> Value {
367
+
fn generate_type_json(ty: &Type, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
337
368
match ty {
338
369
Type::Primitive { kind, .. } => generate_primitive_json(*kind),
339
370
Type::Reference { path, .. } => {
371
+
// Try to resolve this reference in the workspace
372
+
if let Some(resolved_ty) = workspace.resolve_type_reference(path) {
373
+
// Check if this is an inline type by looking in the workspace
374
+
if workspace.is_inline_type(path) {
375
+
// Inline type: expand it recursively
376
+
return generate_type_json(&resolved_ty, usage_counts, workspace, current_namespace);
377
+
}
378
+
}
379
+
380
+
// Not an inline type (or couldn't resolve) - generate a ref
340
381
if path.segments.len() == 1 {
341
382
let name = &path.segments[0].name;
342
-
if should_hoist_alias(name, usage_counts) {
343
-
json!({ "ref": format!("#{}", name) })
344
-
} else {
345
-
json!({ "ref": format!("#{}", name) })
346
-
}
383
+
json!({
384
+
"type": "ref",
385
+
"ref": format!("#{}", name)
386
+
})
347
387
} else {
348
-
json!({ "ref": path.to_string() })
388
+
// Multi-segment path ref
389
+
let namespace = path.segments[..path.segments.len()-1]
390
+
.iter()
391
+
.map(|s| s.name.as_str())
392
+
.collect::<Vec<_>>()
393
+
.join(".");
394
+
let def_name = &path.segments.last().unwrap().name;
395
+
396
+
json!({
397
+
"type": "ref",
398
+
"ref": format!("{}#{}", namespace, def_name)
399
+
})
349
400
}
350
401
}
351
402
Type::Array { inner, .. } => {
352
403
json!({
353
404
"type": "array",
354
-
"items": generate_type_json(inner, usage_counts)
405
+
"items": generate_type_json(inner, usage_counts, workspace, current_namespace)
355
406
})
356
407
}
357
408
Type::Union { types, .. } => {
358
409
let refs: Vec<Value> = types
359
410
.iter()
360
-
.map(|t| generate_type_json(t, usage_counts))
411
+
.map(|t| generate_type_json(t, usage_counts, workspace, current_namespace))
361
412
.collect();
362
413
json!({
363
414
"type": "union",
···
374
425
}
375
426
properties.insert(
376
427
field.name.name.clone(),
377
-
generate_type_json(&field.ty, usage_counts),
428
+
generate_type_json(&field.ty, usage_counts, workspace, current_namespace),
378
429
);
379
430
}
380
431
381
-
json!({
382
-
"type": "object",
383
-
"required": required,
384
-
"properties": properties
385
-
})
432
+
let mut obj = Map::new();
433
+
obj.insert("type".to_string(), json!("object"));
434
+
obj.insert("required".to_string(), json!(required));
435
+
obj.insert("properties".to_string(), json!(properties));
436
+
Value::Object(obj)
437
+
}
438
+
Type::Parenthesized { inner, .. } => {
439
+
// Parentheses are just for grouping - unwrap and process inner type
440
+
generate_type_json(inner, usage_counts, workspace, current_namespace)
386
441
}
387
442
Type::Constrained { base, constraints, .. } => {
388
-
let mut base_json = generate_type_json(base, usage_counts);
443
+
let mut base_json = generate_type_json(base, usage_counts, workspace, current_namespace);
389
444
390
445
if let Some(obj) = base_json.as_object_mut() {
391
446
for constraint in constraints {
···
437
492
obj.insert("format".to_string(), json!(value));
438
493
}
439
494
Constraint::Enum { values, .. } => {
440
-
obj.insert("enum".to_string(), json!(values));
495
+
let enum_vals: Vec<String> = values
496
+
.iter()
497
+
.map(|v| match v {
498
+
mlf_lang::ast::ValueRef::Literal(s) => s.clone(),
499
+
mlf_lang::ast::ValueRef::Reference(path) => path.to_string(),
500
+
})
501
+
.collect();
502
+
obj.insert("enum".to_string(), json!(enum_vals));
441
503
}
442
504
Constraint::KnownValues { values, .. } => {
443
-
let known_vals: Vec<String> = values.iter().map(|path| path.to_string()).collect();
505
+
let known_vals: Vec<String> = values
506
+
.iter()
507
+
.map(|v| match v {
508
+
mlf_lang::ast::ValueRef::Literal(s) => s.clone(),
509
+
mlf_lang::ast::ValueRef::Reference(path) => path.to_string(),
510
+
})
511
+
.collect();
444
512
obj.insert("knownValues".to_string(), json!(known_vals));
445
513
}
446
514
Constraint::Accept { mimes, .. } => {
···
454
522
ConstraintValue::String(s) => json!(s),
455
523
ConstraintValue::Integer(i) => json!(i),
456
524
ConstraintValue::Boolean(b) => json!(b),
525
+
ConstraintValue::Reference(path) => json!(path.to_string()),
457
526
};
458
527
obj.insert("default".to_string(), default_val);
528
+
}
529
+
Constraint::Const { value, .. } => {
530
+
let const_val = match value {
531
+
ConstraintValue::String(s) => json!(s),
532
+
ConstraintValue::Integer(i) => json!(i),
533
+
ConstraintValue::Boolean(b) => json!(b),
534
+
ConstraintValue::Reference(path) => json!(path.to_string()),
535
+
};
536
+
obj.insert("const".to_string(), const_val);
459
537
}
460
538
}
461
539
}
+8
mlf-diagnostics/src/lib.rs
+8
mlf-diagnostics/src/lib.rs
···
150
150
ValidationError::ConstraintTooPermissive { message, .. } => {
151
151
write!(f, "Constraint is too permissive: {}", message)
152
152
}
153
+
ValidationError::ReservedName { name, .. } => {
154
+
write!(f, "Reserved name '{}' cannot be used as an item name", name)
155
+
}
153
156
}
154
157
}
155
158
···
160
163
ValidationError::InvalidConstraint { .. } => "mlf::invalid_constraint",
161
164
ValidationError::TypeMismatch { .. } => "mlf::type_mismatch",
162
165
ValidationError::ConstraintTooPermissive { .. } => "mlf::constraint_too_permissive",
166
+
ValidationError::ReservedName { .. } => "mlf::reserved_name",
163
167
}
164
168
}
165
169
···
192
196
ValidationError::ConstraintTooPermissive { span, message } => {
193
197
vec![LabeledSpan::at(span.start..span.end, message.clone())]
194
198
}
199
+
ValidationError::ReservedName { span, name } => vec![LabeledSpan::at(
200
+
span.start..span.end,
201
+
format!("'{}' is a reserved name and cannot be used", name),
202
+
)],
195
203
}
196
204
}
197
205
+54
-18
mlf-lang/src/ast.rs
+54
-18
mlf-lang/src/ast.rs
···
31
31
#[derive(Debug, Clone, PartialEq)]
32
32
pub enum Item {
33
33
Record(Record),
34
-
Alias(Alias),
34
+
InlineType(InlineType),
35
+
DefType(DefType),
35
36
Token(Token),
36
37
Query(Query),
37
38
Procedure(Procedure),
38
39
Subscription(Subscription),
39
-
Namespace(Namespace),
40
40
Use(Use),
41
41
}
42
42
···
44
44
fn span(&self) -> Span {
45
45
match self {
46
46
Item::Record(r) => r.span,
47
-
Item::Alias(a) => a.span,
47
+
Item::InlineType(i) => i.span,
48
+
Item::DefType(d) => d.span,
48
49
Item::Token(t) => t.span,
49
50
Item::Query(q) => q.span,
50
51
Item::Procedure(p) => p.span,
51
52
Item::Subscription(s) => s.span,
52
-
Item::Namespace(n) => n.span,
53
53
Item::Use(u) => u.span,
54
54
}
55
55
}
···
106
106
pub span: Span,
107
107
}
108
108
109
-
/// A type alias
109
+
/// An inline type definition (expands at point of use)
110
110
#[derive(Debug, Clone, PartialEq)]
111
-
pub struct Alias {
111
+
pub struct InlineType {
112
+
pub docs: Vec<DocComment>,
113
+
pub annotations: Vec<Annotation>,
114
+
pub name: Ident,
115
+
pub ty: Type,
116
+
pub span: Span,
117
+
}
118
+
119
+
/// A def type definition (becomes a named def in lexicon)
120
+
#[derive(Debug, Clone, PartialEq)]
121
+
pub struct DefType {
112
122
pub docs: Vec<DocComment>,
113
123
pub annotations: Vec<Annotation>,
114
124
pub name: Ident,
···
171
181
},
172
182
}
173
183
184
+
impl ReturnType {
185
+
pub fn span(&self) -> Span {
186
+
match self {
187
+
ReturnType::Type(ty) => ty.span(),
188
+
ReturnType::TypeWithErrors { span, .. } => *span,
189
+
}
190
+
}
191
+
}
192
+
174
193
/// An error definition in a query/procedure
175
194
#[derive(Debug, Clone, PartialEq)]
176
195
pub struct ErrorDef {
···
179
198
pub span: Span,
180
199
}
181
200
182
-
/// A namespace block
183
-
#[derive(Debug, Clone, PartialEq)]
184
-
pub struct Namespace {
185
-
pub name: Ident, // e.g., ".actor"
186
-
pub items: Vec<Item>,
187
-
pub span: Span,
188
-
}
189
-
190
201
/// A use statement
191
202
#[derive(Debug, Clone, PartialEq)]
192
203
pub struct Use {
···
241
252
Union { types: Vec<Type>, span: Span },
242
253
/// Object type (inline)
243
254
Object { fields: Vec<Field>, span: Span },
255
+
/// Parenthesized type (for grouping, e.g., (A | B)[])
256
+
Parenthesized { inner: Box<Type>, span: Span },
244
257
/// Constrained type
245
258
Constrained {
246
259
base: Box<Type>,
···
259
272
Type::Array { span, .. } => *span,
260
273
Type::Union { span, .. } => *span,
261
274
Type::Object { span, .. } => *span,
275
+
Type::Parenthesized { span, .. } => *span,
262
276
Type::Constrained { span, .. } => *span,
263
277
Type::Unknown { span } => *span,
264
278
}
···
286
300
MinGraphemes { value: usize, span: Span },
287
301
MaxGraphemes { value: usize, span: Span },
288
302
Format { value: String, span: Span },
289
-
Enum { values: Vec<String>, span: Span },
290
-
KnownValues { values: Vec<Path>, span: Span },
303
+
Enum { values: Vec<ValueRef>, span: Span },
304
+
KnownValues { values: Vec<ValueRef>, span: Span },
291
305
292
306
// Numeric constraints
293
307
Minimum { value: i64, span: Span },
···
297
311
Accept { mimes: Vec<String>, span: Span },
298
312
MaxSize { value: usize, span: Span },
299
313
300
-
// Default value
314
+
// Value constraints
301
315
Default { value: ConstraintValue, span: Span },
316
+
Const { value: ConstraintValue, span: Span },
302
317
}
303
318
304
319
impl Spanned for Constraint {
···
316
331
Constraint::Accept { span, .. } => *span,
317
332
Constraint::MaxSize { span, .. } => *span,
318
333
Constraint::Default { span, .. } => *span,
334
+
Constraint::Const { span, .. } => *span,
319
335
}
320
336
}
321
337
}
322
338
323
-
/// A value in a constraint default
339
+
/// A value in a constraint default - can be a literal or reference
324
340
#[derive(Debug, Clone, PartialEq)]
325
341
pub enum ConstraintValue {
326
342
String(String),
327
343
Integer(i64),
328
344
Boolean(bool),
345
+
/// Reference to a named item (token, record, alias, etc.)
346
+
Reference(Path),
347
+
}
348
+
349
+
/// A value reference in enum/knownValues constraints - can be a string literal or reference to any named item
350
+
#[derive(Debug, Clone, PartialEq)]
351
+
pub enum ValueRef {
352
+
/// String literal (e.g., "open")
353
+
Literal(String),
354
+
/// Reference to a named item - token, record, alias, etc. (e.g., open)
355
+
Reference(Path),
356
+
}
357
+
358
+
impl core::fmt::Display for ValueRef {
359
+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
360
+
match self {
361
+
ValueRef::Literal(s) => write!(f, "\"{}\"", s),
362
+
ValueRef::Reference(path) => write!(f, "{}", path.to_string()),
363
+
}
364
+
}
329
365
}
+1
mlf-lang/src/error.rs
+1
mlf-lang/src/error.rs
+9
-3
mlf-lang/src/lexer.rs
+9
-3
mlf-lang/src/lexer.rs
···
15
15
#[derive(Debug, Clone, PartialEq)]
16
16
pub enum Token {
17
17
// Keywords
18
-
Alias,
19
18
As,
20
19
Blob,
21
20
Boolean,
22
21
Bytes,
23
22
Constrained,
23
+
Def,
24
24
Error,
25
+
Inline,
25
26
Integer,
26
27
Namespace,
27
28
Null,
···
32
33
String,
33
34
Subscription,
34
35
Token,
36
+
Type,
35
37
Unknown,
36
38
Use,
37
39
···
68
70
impl core::fmt::Display for Token {
69
71
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70
72
match self {
71
-
Token::Alias => write!(f, "alias"),
72
73
Token::As => write!(f, "as"),
73
74
Token::Blob => write!(f, "blob"),
74
75
Token::Boolean => write!(f, "boolean"),
75
76
Token::Bytes => write!(f, "bytes"),
76
77
Token::Constrained => write!(f, "constrained"),
78
+
Token::Def => write!(f, "def"),
77
79
Token::Error => write!(f, "error"),
80
+
Token::Inline => write!(f, "inline"),
78
81
Token::Integer => write!(f, "integer"),
79
82
Token::Namespace => write!(f, "namespace"),
80
83
Token::Null => write!(f, "null"),
···
85
88
Token::String => write!(f, "string"),
86
89
Token::Subscription => write!(f, "subscription"),
87
90
Token::Token => write!(f, "token"),
91
+
Token::Type => write!(f, "type"),
88
92
Token::Unknown => write!(f, "unknown"),
89
93
Token::Use => write!(f, "use"),
90
94
Token::Ident(s) => write!(f, "{}", s),
···
139
143
)).parse(input)?;
140
144
141
145
let token = match name {
142
-
"alias" => Token::Alias,
143
146
"as" => Token::As,
144
147
"blob" => Token::Blob,
145
148
"boolean" => Token::Boolean,
146
149
"bytes" => Token::Bytes,
147
150
"constrained" => Token::Constrained,
151
+
"def" => Token::Def,
148
152
"error" => Token::Error,
149
153
"false" => Token::False,
154
+
"inline" => Token::Inline,
150
155
"integer" => Token::Integer,
151
156
"namespace" => Token::Namespace,
152
157
"null" => Token::Null,
···
158
163
"subscription" => Token::Subscription,
159
164
"token" => Token::Token,
160
165
"true" => Token::True,
166
+
"type" => Token::Type,
161
167
"unknown" => Token::Unknown,
162
168
"use" => Token::Use,
163
169
_ => Token::Ident(name.into()),
+243
-81
mlf-lang/src/parser.rs
+243
-81
mlf-lang/src/parser.rs
···
61
61
}
62
62
}
63
63
64
+
fn parse_field_name(&mut self) -> Result<Ident, ParseError> {
65
+
let current = self.current();
66
+
// Field names can be identifiers or keywords
67
+
let name = match ¤t.token {
68
+
LexToken::Ident(n) => n.clone(),
69
+
LexToken::Record => "record".into(),
70
+
LexToken::Token => "token".into(),
71
+
LexToken::Inline => "inline".into(),
72
+
LexToken::Def => "def".into(),
73
+
LexToken::Type => "type".into(),
74
+
LexToken::Query => "query".into(),
75
+
LexToken::Procedure => "procedure".into(),
76
+
LexToken::Subscription => "subscription".into(),
77
+
LexToken::Error => "error".into(),
78
+
LexToken::Use => "use".into(),
79
+
LexToken::As => "as".into(),
80
+
LexToken::String => "string".into(),
81
+
LexToken::Integer => "integer".into(),
82
+
LexToken::Number => "number".into(),
83
+
LexToken::Boolean => "boolean".into(),
84
+
LexToken::Null => "null".into(),
85
+
LexToken::Unknown => "unknown".into(),
86
+
LexToken::Constrained => "constrained".into(),
87
+
LexToken::True => "true".into(),
88
+
LexToken::False => "false".into(),
89
+
_ => {
90
+
return Err(ParseError::Syntax {
91
+
message: alloc::format!("Expected field name, found {}", current.token),
92
+
span: current.span,
93
+
});
94
+
}
95
+
};
96
+
let ident = Ident {
97
+
name,
98
+
span: current.span,
99
+
};
100
+
self.advance();
101
+
Ok(ident)
102
+
}
103
+
64
104
fn parse_path(&mut self) -> Result<Path, ParseError> {
65
105
let mut segments = Vec::new();
66
106
let start = self.current().span.start;
···
105
145
106
146
impl Parser {
107
147
fn parse_item(&mut self) -> Result<Item, ParseError> {
108
-
while matches!(self.current().token, LexToken::DocComment(_)) {
148
+
let mut doc_comments = Vec::new();
149
+
while let LexToken::DocComment(comment) = &self.current().token {
150
+
let span = self.current().span;
151
+
doc_comments.push(DocComment {
152
+
text: comment.clone(),
153
+
span,
154
+
});
109
155
self.advance();
110
156
}
111
157
112
158
let annotations = self.parse_annotations()?;
113
159
114
160
match &self.current().token {
115
-
LexToken::Record => self.parse_record(annotations),
116
-
LexToken::Alias => self.parse_alias(annotations),
117
-
LexToken::Token => self.parse_token(annotations),
118
-
LexToken::Query => self.parse_query(annotations),
119
-
LexToken::Procedure => self.parse_procedure(annotations),
120
-
LexToken::Subscription => self.parse_subscription(annotations),
121
-
LexToken::Namespace => self.parse_namespace(),
161
+
LexToken::Record => self.parse_record(doc_comments, annotations),
162
+
LexToken::Inline => self.parse_inline_type(doc_comments, annotations),
163
+
LexToken::Def => self.parse_def_type(doc_comments, annotations),
164
+
LexToken::Token => self.parse_token(doc_comments, annotations),
165
+
LexToken::Query => self.parse_query(doc_comments, annotations),
166
+
LexToken::Procedure => self.parse_procedure(doc_comments, annotations),
167
+
LexToken::Subscription => self.parse_subscription(doc_comments, annotations),
122
168
LexToken::Use => self.parse_use(),
123
169
_ => Err(ParseError::Syntax {
124
170
message: alloc::format!("Expected item definition, found {}", self.current().token),
···
219
265
}
220
266
}
221
267
222
-
fn parse_record(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
268
+
fn parse_record(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
223
269
let start = self.expect(LexToken::Record)?;
224
270
let name = self.parse_ident()?;
225
271
self.expect(LexToken::LeftBrace)?;
226
272
227
273
let mut fields = Vec::new();
228
-
let mut doc_comments = Vec::new();
274
+
let mut field_docs = Vec::new();
229
275
230
276
while !matches!(self.current().token, LexToken::RightBrace) {
231
277
if let LexToken::DocComment(comment) = &self.current().token {
232
278
let span = self.current().span;
233
-
doc_comments.push(DocComment {
279
+
field_docs.push(DocComment {
234
280
text: comment.clone(),
235
281
span,
236
282
});
237
283
self.advance();
238
284
} else {
239
-
fields.push(self.parse_field(doc_comments.clone())?);
240
-
doc_comments.clear();
285
+
fields.push(self.parse_field(field_docs.clone())?);
286
+
field_docs.clear();
241
287
}
242
288
}
243
289
244
290
let end = self.expect(LexToken::RightBrace)?;
245
-
self.expect(LexToken::Semicolon)?;
246
291
247
292
Ok(Item::Record(Record {
248
-
docs: Vec::new(),
293
+
docs,
249
294
annotations,
250
295
name,
251
296
fields,
···
255
300
256
301
fn parse_field(&mut self, docs: Vec<DocComment>) -> Result<Field, ParseError> {
257
302
let annotations = self.parse_annotations()?;
258
-
let name = self.parse_ident()?;
303
+
let name = self.parse_field_name()?;
259
304
260
305
let optional = if matches!(self.current().token, LexToken::Question) {
261
306
self.advance();
···
266
311
267
312
self.expect(LexToken::Colon)?;
268
313
let ty = self.parse_type()?;
269
-
self.expect(LexToken::Comma)?;
314
+
315
+
// Comma is required (unless we're at the end)
316
+
if !matches!(self.current().token, LexToken::RightBrace) {
317
+
self.expect(LexToken::Comma)?;
318
+
} else if matches!(self.current().token, LexToken::Comma) {
319
+
// Allow trailing comma
320
+
self.advance();
321
+
}
270
322
271
323
let span = Span::new(name.span.start, ty.span().end);
272
324
···
280
332
})
281
333
}
282
334
283
-
fn parse_alias(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
284
-
let start = self.expect(LexToken::Alias)?;
335
+
fn parse_inline_type(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
336
+
let start = self.expect(LexToken::Inline)?;
337
+
self.expect(LexToken::Type)?;
338
+
let name = self.parse_ident()?;
339
+
self.expect(LexToken::Equals)?;
340
+
let ty = self.parse_type()?;
341
+
let end = self.expect(LexToken::Semicolon)?;
342
+
343
+
Ok(Item::InlineType(InlineType {
344
+
docs,
345
+
annotations,
346
+
name,
347
+
ty,
348
+
span: Span::new(start.start, end.end),
349
+
}))
350
+
}
351
+
352
+
fn parse_def_type(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
353
+
let start = self.expect(LexToken::Def)?;
354
+
self.expect(LexToken::Type)?;
285
355
let name = self.parse_ident()?;
286
356
self.expect(LexToken::Equals)?;
287
357
let ty = self.parse_type()?;
288
358
let end = self.expect(LexToken::Semicolon)?;
289
359
290
-
Ok(Item::Alias(Alias {
291
-
docs: Vec::new(),
360
+
Ok(Item::DefType(DefType {
361
+
docs,
292
362
annotations,
293
363
name,
294
364
ty,
···
296
366
}))
297
367
}
298
368
299
-
fn parse_token(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
369
+
fn parse_token(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
300
370
let start = self.expect(LexToken::Token)?;
301
371
let name = self.parse_ident()?;
302
372
let end = self.expect(LexToken::Semicolon)?;
303
373
304
374
Ok(Item::Token(Token {
305
-
docs: Vec::new(),
375
+
docs,
306
376
annotations,
307
377
name,
308
378
span: Span::new(start.start, end.end),
309
379
}))
310
380
}
311
381
312
-
fn parse_query(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
382
+
fn parse_query(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
313
383
let start = self.expect(LexToken::Query)?;
314
384
let name = self.parse_ident()?;
315
385
self.expect(LexToken::LeftParen)?;
···
351
421
let end = self.expect(LexToken::Semicolon)?;
352
422
353
423
Ok(Item::Query(Query {
354
-
docs: Vec::new(),
424
+
docs,
355
425
annotations,
356
426
name,
357
427
params,
···
360
430
}))
361
431
}
362
432
363
-
fn parse_procedure(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
433
+
fn parse_procedure(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
364
434
let start = self.expect(LexToken::Procedure)?;
365
435
let name = self.parse_ident()?;
366
436
self.expect(LexToken::LeftParen)?;
···
402
472
let end = self.expect(LexToken::Semicolon)?;
403
473
404
474
Ok(Item::Procedure(Procedure {
405
-
docs: Vec::new(),
475
+
docs,
406
476
annotations,
407
477
name,
408
478
params,
···
411
481
}))
412
482
}
413
483
414
-
fn parse_subscription(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
484
+
fn parse_subscription(&mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>) -> Result<Item, ParseError> {
415
485
let start = self.expect(LexToken::Subscription)?;
416
486
let name = self.parse_ident()?;
417
487
self.expect(LexToken::LeftParen)?;
···
426
496
let end = self.expect(LexToken::Semicolon)?;
427
497
428
498
Ok(Item::Subscription(Subscription {
429
-
docs: Vec::new(),
499
+
docs,
430
500
annotations,
431
501
name,
432
502
params,
···
435
505
}))
436
506
}
437
507
438
-
fn parse_namespace(&mut self) -> Result<Item, ParseError> {
439
-
let start = self.expect(LexToken::Namespace)?;
440
-
let path = self.parse_path()?;
441
-
442
-
// Convert path to a single identifier with dotted name
443
-
let name = Ident {
444
-
name: path.segments.iter().map(|s| s.name.as_str()).collect::<Vec<_>>().join("."),
445
-
span: path.span,
446
-
};
447
-
448
-
let end = self.expect(LexToken::Semicolon)?;
449
-
450
-
Ok(Item::Namespace(Namespace {
451
-
name,
452
-
items: Vec::new(),
453
-
span: Span::new(start.start, end.end),
454
-
}))
455
-
}
456
-
457
508
fn parse_use(&mut self) -> Result<Item, ParseError> {
458
509
let start = self.expect(LexToken::Use)?;
459
510
let path = self.parse_path()?;
···
480
531
481
532
fn parse_params(&mut self) -> Result<Vec<Field>, ParseError> {
482
533
let mut params = Vec::new();
534
+
let mut doc_comments = Vec::new();
483
535
484
536
while !matches!(self.current().token, LexToken::RightParen) {
485
-
let annotations = self.parse_annotations()?;
486
-
let name = self.parse_ident()?;
487
-
488
-
let optional = if matches!(self.current().token, LexToken::Question) {
537
+
if let LexToken::DocComment(comment) = &self.current().token {
538
+
let span = self.current().span;
539
+
doc_comments.push(DocComment {
540
+
text: comment.clone(),
541
+
span,
542
+
});
489
543
self.advance();
490
-
true
491
544
} else {
492
-
false
493
-
};
545
+
let annotations = self.parse_annotations()?;
546
+
let name = self.parse_ident()?;
494
547
495
-
self.expect(LexToken::Colon)?;
496
-
let ty = self.parse_type()?;
548
+
let optional = if matches!(self.current().token, LexToken::Question) {
549
+
self.advance();
550
+
true
551
+
} else {
552
+
false
553
+
};
554
+
555
+
self.expect(LexToken::Colon)?;
556
+
let ty = self.parse_type()?;
497
557
498
-
let span = Span::new(name.span.start, ty.span().end);
558
+
let span = Span::new(name.span.start, ty.span().end);
499
559
500
-
params.push(Field {
501
-
docs: Vec::new(),
502
-
annotations,
503
-
name,
504
-
ty,
505
-
optional,
506
-
span,
507
-
});
560
+
params.push(Field {
561
+
docs: doc_comments.clone(),
562
+
annotations,
563
+
name,
564
+
ty,
565
+
optional,
566
+
span,
567
+
});
568
+
doc_comments.clear();
508
569
509
-
if matches!(self.current().token, LexToken::Comma) {
510
-
self.advance();
511
-
} else {
512
-
break;
570
+
if matches!(self.current().token, LexToken::Comma) {
571
+
self.advance();
572
+
} else {
573
+
break;
574
+
}
513
575
}
514
576
}
515
577
···
533
595
} else {
534
596
let name = self.parse_ident()?;
535
597
let span = name.span;
536
-
self.expect(LexToken::Comma)?;
598
+
599
+
// Comma is required (unless we're at the end)
600
+
if !matches!(self.current().token, LexToken::RightBrace) {
601
+
self.expect(LexToken::Comma)?;
602
+
} else if matches!(self.current().token, LexToken::Comma) {
603
+
// Allow trailing comma
604
+
self.advance();
605
+
}
606
+
537
607
errors.push(ErrorDef {
538
608
docs: doc_comments.clone(),
539
609
name,
···
644
714
LexToken::LeftBrace => {
645
715
self.advance();
646
716
let mut fields = Vec::new();
717
+
let mut doc_comments = Vec::new();
647
718
648
719
while !matches!(self.current().token, LexToken::RightBrace) {
649
-
fields.push(self.parse_field(Vec::new())?);
720
+
if let LexToken::DocComment(comment) = &self.current().token {
721
+
let span = self.current().span;
722
+
doc_comments.push(DocComment {
723
+
text: comment.clone(),
724
+
span,
725
+
});
726
+
self.advance();
727
+
} else {
728
+
fields.push(self.parse_field(doc_comments.clone())?);
729
+
doc_comments.clear();
730
+
}
650
731
}
651
732
652
733
let end = self.expect(LexToken::RightBrace)?;
653
734
Type::Object {
654
735
fields,
736
+
span: Span::new(start, end.end),
737
+
}
738
+
}
739
+
LexToken::LeftParen => {
740
+
self.advance();
741
+
let inner = self.parse_type()?;
742
+
let end = self.expect(LexToken::RightParen)?;
743
+
Type::Parenthesized {
744
+
inner: alloc::boxed::Box::new(inner),
655
745
span: Span::new(start, end.end),
656
746
}
657
747
}
···
682
772
while !matches!(self.current().token, LexToken::RightBrace) {
683
773
constraints.push(self.parse_constraint()?);
684
774
685
-
if matches!(self.current().token, LexToken::Comma) {
775
+
// Comma is required (unless we're at the end)
776
+
if !matches!(self.current().token, LexToken::RightBrace) {
777
+
self.expect(LexToken::Comma)?;
778
+
} else if matches!(self.current().token, LexToken::Comma) {
779
+
// Allow trailing comma
686
780
self.advance();
687
-
} else {
688
-
break;
689
781
}
690
782
}
691
783
···
777
869
778
870
while !matches!(self.current().token, LexToken::RightBracket) {
779
871
let current = self.current();
780
-
match ¤t.token {
872
+
// Accept either string literals or identifier paths
873
+
let value_ref = match ¤t.token {
781
874
LexToken::StringLit(s) => {
782
-
values.push(s.clone());
875
+
let v = ValueRef::Literal(s.clone());
783
876
self.advance();
877
+
v
878
+
}
879
+
LexToken::Ident(_) => {
880
+
let path = self.parse_path()?;
881
+
ValueRef::Reference(path)
784
882
}
785
883
_ => {
786
884
return Err(ParseError::Syntax {
787
-
message: alloc::format!("Expected string literal in enum"),
885
+
message: alloc::format!("Expected string literal or identifier in enum"),
788
886
span: current.span,
789
887
});
790
888
}
791
-
}
889
+
};
890
+
values.push(value_ref);
792
891
793
892
if matches!(self.current().token, LexToken::Comma) {
794
893
self.advance();
···
914
1013
let mut values = Vec::new();
915
1014
916
1015
while !matches!(self.current().token, LexToken::RightBracket) {
917
-
values.push(self.parse_path()?);
1016
+
let current = self.current();
1017
+
// Accept either string literals or identifier paths
1018
+
let value_ref = match ¤t.token {
1019
+
LexToken::StringLit(s) => {
1020
+
let v = ValueRef::Literal(s.clone());
1021
+
self.advance();
1022
+
v
1023
+
}
1024
+
LexToken::Ident(_) => {
1025
+
let path = self.parse_path()?;
1026
+
ValueRef::Reference(path)
1027
+
}
1028
+
_ => {
1029
+
return Err(ParseError::Syntax {
1030
+
message: alloc::format!("Expected string literal or identifier in knownValues"),
1031
+
span: current.span,
1032
+
});
1033
+
}
1034
+
};
1035
+
values.push(value_ref);
918
1036
919
1037
if matches!(self.current().token, LexToken::Comma) {
920
1038
self.advance();
···
959
1077
self.advance();
960
1078
v
961
1079
}
1080
+
LexToken::Ident(_) => {
1081
+
let path = self.parse_path()?;
1082
+
ConstraintValue::Reference(path)
1083
+
}
962
1084
_ => {
963
1085
return Err(ParseError::Syntax {
964
-
message: alloc::format!("Expected string, integer, or boolean for default"),
1086
+
message: alloc::format!("Expected string, integer, boolean, or identifier for default"),
965
1087
span: current_span,
966
1088
});
967
1089
}
968
1090
};
969
1091
Constraint::Default {
1092
+
value,
1093
+
span: Span::new(start, end_span),
1094
+
}
1095
+
}
1096
+
"const" => {
1097
+
use crate::ast::ConstraintValue;
1098
+
let end_span = current_span.end;
1099
+
let value = match ¤t.token {
1100
+
LexToken::StringLit(s) => {
1101
+
let v = ConstraintValue::String(s.clone());
1102
+
self.advance();
1103
+
v
1104
+
}
1105
+
LexToken::IntLit(i) => {
1106
+
let v = ConstraintValue::Integer(*i);
1107
+
self.advance();
1108
+
v
1109
+
}
1110
+
LexToken::True => {
1111
+
let v = ConstraintValue::Boolean(true);
1112
+
self.advance();
1113
+
v
1114
+
}
1115
+
LexToken::False => {
1116
+
let v = ConstraintValue::Boolean(false);
1117
+
self.advance();
1118
+
v
1119
+
}
1120
+
LexToken::Ident(_) => {
1121
+
let path = self.parse_path()?;
1122
+
ConstraintValue::Reference(path)
1123
+
}
1124
+
_ => {
1125
+
return Err(ParseError::Syntax {
1126
+
message: alloc::format!("Expected string, integer, boolean, or identifier for const"),
1127
+
span: current_span,
1128
+
});
1129
+
}
1130
+
};
1131
+
Constraint::Const {
970
1132
value,
971
1133
span: Span::new(start, end_span),
972
1134
}
+159
-24
mlf-lang/src/workspace.rs
+159
-24
mlf-lang/src/workspace.rs
···
141
141
142
142
fn typecheck_item(&self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> {
143
143
match item {
144
-
Item::Alias(a) => self.typecheck_alias(namespace, a),
144
+
Item::InlineType(i) => self.typecheck_inline_type(namespace, i),
145
+
Item::DefType(d) => self.typecheck_def_type(namespace, d),
145
146
Item::Record(r) => self.typecheck_record(namespace, r),
146
147
_ => Ok(()),
147
148
}
148
149
}
149
150
150
-
fn typecheck_alias(&self, namespace: &str, alias: &Alias) -> Result<(), ValidationErrors> {
151
-
self.typecheck_type(namespace, &alias.ty)
151
+
fn typecheck_inline_type(&self, namespace: &str, inline_type: &InlineType) -> Result<(), ValidationErrors> {
152
+
self.typecheck_type(namespace, &inline_type.ty)
153
+
}
154
+
155
+
fn typecheck_def_type(&self, namespace: &str, def_type: &DefType) -> Result<(), ValidationErrors> {
156
+
self.typecheck_type(namespace, &def_type.ty)
152
157
}
153
158
154
159
fn typecheck_record(&self, namespace: &str, record: &Record) -> Result<(), ValidationErrors> {
···
209
214
Err(errors)
210
215
}
211
216
}
217
+
Type::Parenthesized { inner, .. } => self.typecheck_type(namespace, inner),
212
218
Type::Constrained { base, constraints, span } => {
213
219
let mut errors = ValidationErrors::new();
214
220
···
273
279
}
274
280
Constraint::KnownValues { .. } => {}
275
281
Constraint::Default { .. } => {}
282
+
Constraint::Const { .. } => {}
276
283
}
277
284
}
278
285
···
434
441
all_constraints.extend(self.get_base_constraints(base));
435
442
all_constraints
436
443
}
444
+
Type::Parenthesized { inner, .. } => self.get_base_constraints(inner),
437
445
Type::Reference { path, .. } => {
438
446
if let Some(resolved_ty) = self.resolve_type_reference(path) {
439
447
self.get_base_constraints(&resolved_ty)
···
449
457
match ty {
450
458
Type::Primitive { kind, .. } => Some(*kind),
451
459
Type::Constrained { base, .. } => self.get_base_primitive(base),
460
+
Type::Parenthesized { inner, .. } => self.get_base_primitive(inner),
452
461
Type::Reference { path, .. } => {
453
462
if let Some(resolved_ty) = self.resolve_type_reference(path) {
454
463
self.get_base_primitive(&resolved_ty)
···
460
469
}
461
470
}
462
471
463
-
fn resolve_type_reference(&self, path: &Path) -> Option<Type> {
472
+
pub fn resolve_type_reference(&self, path: &Path) -> Option<Type> {
464
473
if path.segments.len() == 1 {
465
474
let name = &path.segments[0].name;
466
475
467
476
for (_, module) in &self.modules {
468
477
if let Some(Symbol::Alias { .. }) = module.symbols.types.get(name) {
469
478
for item in &module.lexicon.items {
470
-
if let Item::Alias(a) = item {
471
-
if a.name.name == *name {
472
-
return Some(a.ty.clone());
479
+
match item {
480
+
Item::InlineType(i) if i.name.name == *name => {
481
+
return Some(i.ty.clone());
473
482
}
483
+
Item::DefType(d) if d.name.name == *name => {
484
+
return Some(d.ty.clone());
485
+
}
486
+
_ => {}
474
487
}
475
488
}
476
489
}
···
485
498
486
499
if let Some(module) = self.modules.get(&target_namespace) {
487
500
for item in &module.lexicon.items {
488
-
if let Item::Alias(a) = item {
489
-
if a.name.name == *type_name {
490
-
return Some(a.ty.clone());
501
+
match item {
502
+
Item::InlineType(i) if i.name.name == *type_name => {
503
+
return Some(i.ty.clone());
504
+
}
505
+
Item::DefType(d) if d.name.name == *type_name => {
506
+
return Some(d.ty.clone());
491
507
}
508
+
_ => {}
492
509
}
493
510
}
494
511
}
···
497
514
None
498
515
}
499
516
517
+
pub fn is_inline_type(&self, path: &Path) -> bool {
518
+
if path.segments.len() == 1 {
519
+
let name = &path.segments[0].name;
520
+
521
+
for (_, module) in &self.modules {
522
+
if let Some(Symbol::Alias { .. }) = module.symbols.types.get(name) {
523
+
for item in &module.lexicon.items {
524
+
if let Item::InlineType(i) = item {
525
+
if i.name.name == *name {
526
+
return true;
527
+
}
528
+
}
529
+
}
530
+
}
531
+
}
532
+
} else {
533
+
let target_namespace = path.segments[..path.segments.len() - 1]
534
+
.iter()
535
+
.map(|s| s.name.as_str())
536
+
.collect::<Vec<_>>()
537
+
.join(".");
538
+
let type_name = &path.segments[path.segments.len() - 1].name;
539
+
540
+
if let Some(module) = self.modules.get(&target_namespace) {
541
+
for item in &module.lexicon.items {
542
+
if let Item::InlineType(i) = item {
543
+
if i.name.name == *type_name {
544
+
return true;
545
+
}
546
+
}
547
+
}
548
+
}
549
+
}
550
+
551
+
false
552
+
}
553
+
500
554
fn resolve_imports(&mut self) -> Result<(), ValidationErrors> {
501
555
let mut errors = ValidationErrors::new();
502
556
···
624
678
for item in &lexicon.items {
625
679
match item {
626
680
Item::Record(r) => {
681
+
// Check for reserved names
682
+
if r.name.name == "main" || r.name.name == "defs" {
683
+
errors.push(crate::error::ValidationError::ReservedName {
684
+
name: r.name.name.clone(),
685
+
span: r.name.span,
686
+
});
687
+
}
688
+
627
689
if let Some(existing) = symbols.types.get(&r.name.name) {
628
690
errors.push(crate::error::ValidationError::DuplicateDefinition {
629
691
name: r.name.name.clone(),
···
640
702
);
641
703
}
642
704
}
643
-
Item::Alias(a) => {
644
-
if let Some(existing) = symbols.types.get(&a.name.name) {
705
+
Item::InlineType(i) => {
706
+
// Check for reserved names
707
+
if i.name.name == "main" || i.name.name == "defs" {
708
+
errors.push(crate::error::ValidationError::ReservedName {
709
+
name: i.name.name.clone(),
710
+
span: i.name.span,
711
+
});
712
+
}
713
+
714
+
if let Some(existing) = symbols.types.get(&i.name.name) {
645
715
errors.push(crate::error::ValidationError::DuplicateDefinition {
646
-
name: a.name.name.clone(),
716
+
name: i.name.name.clone(),
647
717
first_span: existing.span(),
648
-
second_span: a.name.span,
718
+
second_span: i.name.span,
649
719
});
650
720
} else {
651
721
symbols.types.insert(
652
-
a.name.name.clone(),
722
+
i.name.name.clone(),
653
723
Symbol::Alias {
654
-
name: a.name.name.clone(),
655
-
span: a.name.span,
724
+
name: i.name.name.clone(),
725
+
span: i.name.span,
726
+
},
727
+
);
728
+
}
729
+
}
730
+
Item::DefType(d) => {
731
+
// Check for reserved names
732
+
if d.name.name == "main" || d.name.name == "defs" {
733
+
errors.push(crate::error::ValidationError::ReservedName {
734
+
name: d.name.name.clone(),
735
+
span: d.name.span,
736
+
});
737
+
}
738
+
739
+
if let Some(existing) = symbols.types.get(&d.name.name) {
740
+
errors.push(crate::error::ValidationError::DuplicateDefinition {
741
+
name: d.name.name.clone(),
742
+
first_span: existing.span(),
743
+
second_span: d.name.span,
744
+
});
745
+
} else {
746
+
symbols.types.insert(
747
+
d.name.name.clone(),
748
+
Symbol::Alias {
749
+
name: d.name.name.clone(),
750
+
span: d.name.span,
656
751
},
657
752
);
658
753
}
659
754
}
660
755
Item::Token(t) => {
756
+
// Check for reserved names
757
+
if t.name.name == "main" || t.name.name == "defs" {
758
+
errors.push(crate::error::ValidationError::ReservedName {
759
+
name: t.name.name.clone(),
760
+
span: t.name.span,
761
+
});
762
+
}
763
+
661
764
if let Some(existing) = symbols.types.get(&t.name.name) {
662
765
errors.push(crate::error::ValidationError::DuplicateDefinition {
663
766
name: t.name.name.clone(),
···
674
777
);
675
778
}
676
779
}
677
-
Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => {
678
-
// These don't define types, so skip
780
+
Item::Query(q) => {
781
+
// Check for reserved names
782
+
if q.name.name == "main" || q.name.name == "defs" {
783
+
errors.push(crate::error::ValidationError::ReservedName {
784
+
name: q.name.name.clone(),
785
+
span: q.name.span,
786
+
});
787
+
}
788
+
}
789
+
Item::Procedure(p) => {
790
+
// Check for reserved names
791
+
if p.name.name == "main" || p.name.name == "defs" {
792
+
errors.push(crate::error::ValidationError::ReservedName {
793
+
name: p.name.name.clone(),
794
+
span: p.name.span,
795
+
});
796
+
}
679
797
}
680
-
Item::Namespace(_) | Item::Use(_) => {
798
+
Item::Subscription(s) => {
799
+
// Check for reserved names
800
+
if s.name.name == "main" || s.name.name == "defs" {
801
+
errors.push(crate::error::ValidationError::ReservedName {
802
+
name: s.name.name.clone(),
803
+
span: s.name.span,
804
+
});
805
+
}
806
+
}
807
+
Item::Use(_) => {
681
808
// Handled separately
682
809
}
683
810
}
···
709
836
fn resolve_item(&self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> {
710
837
match item {
711
838
Item::Record(r) => self.resolve_record(namespace, r),
712
-
Item::Alias(a) => self.resolve_alias(namespace, a),
839
+
Item::InlineType(i) => self.resolve_inline_type(namespace, i),
840
+
Item::DefType(d) => self.resolve_def_type(namespace, d),
713
841
Item::Query(q) => self.resolve_query(namespace, q),
714
842
Item::Procedure(p) => self.resolve_procedure(namespace, p),
715
843
Item::Subscription(s) => self.resolve_subscription(namespace, s),
716
-
Item::Token(_) | Item::Namespace(_) | Item::Use(_) => Ok(()),
844
+
Item::Token(_) | Item::Use(_) => Ok(()),
717
845
}
718
846
}
719
847
···
733
861
}
734
862
}
735
863
736
-
fn resolve_alias(&self, namespace: &str, alias: &Alias) -> Result<(), ValidationErrors> {
737
-
self.resolve_type(namespace, &alias.ty)
864
+
fn resolve_inline_type(&self, namespace: &str, inline_type: &InlineType) -> Result<(), ValidationErrors> {
865
+
self.resolve_type(namespace, &inline_type.ty)
866
+
}
867
+
868
+
fn resolve_def_type(&self, namespace: &str, def_type: &DefType) -> Result<(), ValidationErrors> {
869
+
self.resolve_type(namespace, &def_type.ty)
738
870
}
739
871
740
872
fn resolve_query(&self, namespace: &str, query: &Query) -> Result<(), ValidationErrors> {
···
849
981
} else {
850
982
Err(errors)
851
983
}
984
+
}
985
+
Type::Parenthesized { inner, .. } => {
986
+
self.resolve_type(namespace, inner)
852
987
}
853
988
Type::Constrained { base, .. } => {
854
989
self.resolve_type(namespace, base)
+20
-6
mlf-validation/src/lib.rs
+20
-6
mlf-validation/src/lib.rs
···
97
97
Type::Union { types, .. } => {
98
98
self.validate_union(value, types, path, errors);
99
99
}
100
+
Type::Parenthesized { inner, .. } => {
101
+
self.validate_against_type(value, inner, path, errors);
102
+
}
100
103
Type::Reference { path: ref_path, .. } => {
101
104
// Try to resolve reference
102
105
if let Some(resolved_type) = self.resolve_reference(ref_path) {
···
112
115
}
113
116
114
117
fn resolve_reference(&self, path: &Path) -> Option<Type> {
115
-
// Simple resolution: look for aliases with matching name
118
+
// Simple resolution: look for inline/def types with matching name
116
119
if path.segments.len() == 1 {
117
120
let name = &path.segments[0].name;
118
121
for item in &self.lexicon.items {
119
-
if let Item::Alias(alias) = item {
120
-
if alias.name.name == *name {
121
-
return Some(alias.ty.clone());
122
+
match item {
123
+
Item::InlineType(i) if i.name.name == *name => {
124
+
return Some(i.ty.clone());
122
125
}
126
+
Item::DefType(d) if d.name.name == *name => {
127
+
return Some(d.ty.clone());
128
+
}
129
+
_ => {}
123
130
}
124
131
}
125
132
}
···
300
307
}
301
308
Constraint::Enum { values, .. } => {
302
309
if let Some(s) = value.as_str() {
303
-
if !values.contains(&s.to_string()) {
310
+
let enum_strings: Vec<String> = values.iter().map(|v| match v {
311
+
mlf_lang::ast::ValueRef::Literal(lit) => lit.clone(),
312
+
mlf_lang::ast::ValueRef::Reference(path) => path.to_string(),
313
+
}).collect();
314
+
if !enum_strings.contains(&s.to_string()) {
304
315
errors.push(ValidationError {
305
316
path: path.to_string(),
306
-
message: format!("Value '{}' not in enum: {:?}", s, values),
317
+
message: format!("Value '{}' not in enum: {:?}", s, enum_strings),
307
318
});
308
319
}
309
320
}
···
344
355
}
345
356
Constraint::Default { .. } => {
346
357
// Default values are used when field is missing, not for validation
358
+
}
359
+
Constraint::Const { .. } => {
360
+
// Const values are enforced at compile time, not runtime validation
347
361
}
348
362
}
349
363
}
+34
-1
mlf-wasm/src/lib.rs
+34
-1
mlf-wasm/src/lib.rs
···
106
106
}
107
107
};
108
108
109
+
// Create workspace with prelude for type resolution
110
+
let mut workspace = match mlf_lang::Workspace::with_prelude() {
111
+
Ok(ws) => ws,
112
+
Err(e) => {
113
+
let result = GenerateResult {
114
+
success: false,
115
+
lexicon: None,
116
+
error: Some(format!("Failed to load prelude: {:?}", e)),
117
+
};
118
+
return serde_wasm_bindgen::to_value(&result).unwrap();
119
+
}
120
+
};
121
+
122
+
// Add the module to workspace for resolution
123
+
if let Err(e) = workspace.add_module(namespace.to_string(), lexicon.clone()) {
124
+
let result = GenerateResult {
125
+
success: false,
126
+
lexicon: None,
127
+
error: Some(format!("Failed to add module: {:?}", e)),
128
+
};
129
+
return serde_wasm_bindgen::to_value(&result).unwrap();
130
+
}
131
+
132
+
// Resolve types (expands inline types from prelude)
133
+
if let Err(e) = workspace.resolve() {
134
+
let result = GenerateResult {
135
+
success: false,
136
+
lexicon: None,
137
+
error: Some(format!("Type resolution error: {:?}", e)),
138
+
};
139
+
return serde_wasm_bindgen::to_value(&result).unwrap();
140
+
}
141
+
109
142
// Generate JSON lexicon
110
-
let json_lexicon = mlf_codegen::generate_lexicon(namespace, &lexicon);
143
+
let json_lexicon = mlf_codegen::generate_lexicon(namespace, &lexicon, &workspace);
111
144
112
145
match serde_json::to_string_pretty(&json_lexicon) {
113
146
Ok(json_str) => {
+11
-11
resources/prelude.mlf
+11
-11
resources/prelude.mlf
···
1
-
alias AtIdentifier = string constrained {
1
+
inline type AtIdentifier = string constrained {
2
2
format: "at-identifier",
3
3
};
4
4
5
-
alias AtUri = string constrained {
5
+
inline type AtUri = string constrained {
6
6
format: "at-uri",
7
7
};
8
8
9
-
alias Cid = string constrained {
9
+
inline type Cid = string constrained {
10
10
format: "cid",
11
11
};
12
12
13
-
alias Datetime = string constrained {
13
+
inline type Datetime = string constrained {
14
14
format: "datetime",
15
15
};
16
16
17
-
alias Did = string constrained {
17
+
inline type Did = string constrained {
18
18
format: "did",
19
19
};
20
20
21
-
alias Handle = string constrained {
21
+
inline type Handle = string constrained {
22
22
format: "handle",
23
23
};
24
24
25
-
alias Nsid = string constrained {
25
+
inline type Nsid = string constrained {
26
26
format: "nsid",
27
27
};
28
28
29
-
alias Tid = string constrained {
29
+
inline type Tid = string constrained {
30
30
format: "tid",
31
31
};
32
32
33
-
alias RecordKey = string constrained {
33
+
inline type RecordKey = string constrained {
34
34
format: "record-key",
35
35
};
36
36
37
-
alias Uri = string constrained {
37
+
inline type Uri = string constrained {
38
38
format: "uri",
39
39
};
40
40
41
-
alias Language = string constrained {
41
+
inline type Language = string constrained {
42
42
format: "language",
43
43
};
+17
-16
tree-sitter-mlf/grammar.js
+17
-16
tree-sitter-mlf/grammar.js
···
20
20
source_file: $ => repeat($.item),
21
21
22
22
item: $ => choice(
23
-
$.namespace_declaration,
24
23
$.use_statement,
25
24
$.record_definition,
26
-
$.alias_definition,
25
+
$.inline_type_definition,
26
+
$.def_type_definition,
27
27
$.token_definition,
28
28
$.query_definition,
29
29
$.procedure_definition,
···
34
34
doc_comment: $ => token(seq('///', /.*/)),
35
35
comment: $ => token(seq('//', /.*/)),
36
36
37
-
// Namespace
38
-
namespace_declaration: $ => seq(
39
-
'namespace',
40
-
field('name', $.namespace_identifier),
41
-
';'
42
-
),
43
-
44
-
namespace_identifier: $ => /[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*/,
45
-
46
37
// Use statements
47
38
use_statement: $ => seq(
48
39
'use',
···
54
45
record_definition: $ => seq(
55
46
'record',
56
47
field('name', $.identifier),
57
-
field('body', $.record_body),
58
-
';'
48
+
field('body', $.record_body)
59
49
),
60
50
61
51
record_body: $ => seq(
···
73
63
','
74
64
),
75
65
76
-
// Alias definition
77
-
alias_definition: $ => seq(
78
-
'alias',
66
+
// Inline type definition
67
+
inline_type_definition: $ => seq(
68
+
'inline',
69
+
'type',
70
+
field('name', $.identifier),
71
+
'=',
72
+
field('type', $.type),
73
+
';'
74
+
),
75
+
76
+
// Def type definition
77
+
def_type_definition: $ => seq(
78
+
'def',
79
+
'type',
79
80
field('name', $.identifier),
80
81
'=',
81
82
field('type', $.type),
+48
-3
tree-sitter-mlf/src/grammar.json
+48
-3
tree-sitter-mlf/src/grammar.json
···
25
25
},
26
26
{
27
27
"type": "SYMBOL",
28
-
"name": "alias_definition"
28
+
"name": "inline_type_definition"
29
+
},
30
+
{
31
+
"type": "SYMBOL",
32
+
"name": "def_type_definition"
29
33
},
30
34
{
31
35
"type": "SYMBOL",
···
225
229
}
226
230
]
227
231
},
228
-
"alias_definition": {
232
+
"inline_type_definition": {
233
+
"type": "SEQ",
234
+
"members": [
235
+
{
236
+
"type": "STRING",
237
+
"value": "inline"
238
+
},
239
+
{
240
+
"type": "STRING",
241
+
"value": "type"
242
+
},
243
+
{
244
+
"type": "FIELD",
245
+
"name": "name",
246
+
"content": {
247
+
"type": "SYMBOL",
248
+
"name": "identifier"
249
+
}
250
+
},
251
+
{
252
+
"type": "STRING",
253
+
"value": "="
254
+
},
255
+
{
256
+
"type": "FIELD",
257
+
"name": "type",
258
+
"content": {
259
+
"type": "SYMBOL",
260
+
"name": "type"
261
+
}
262
+
},
263
+
{
264
+
"type": "STRING",
265
+
"value": ";"
266
+
}
267
+
]
268
+
},
269
+
"def_type_definition": {
229
270
"type": "SEQ",
230
271
"members": [
231
272
{
232
273
"type": "STRING",
233
-
"value": "alias"
274
+
"value": "def"
275
+
},
276
+
{
277
+
"type": "STRING",
278
+
"value": "type"
234
279
},
235
280
{
236
281
"type": "FIELD",
+71
-33
tree-sitter-mlf/src/node-types.json
+71
-33
tree-sitter-mlf/src/node-types.json
···
1
1
[
2
2
{
3
-
"type": "alias_definition",
4
-
"named": true,
5
-
"fields": {
6
-
"name": {
7
-
"multiple": false,
8
-
"required": true,
9
-
"types": [
10
-
{
11
-
"type": "identifier",
12
-
"named": true
13
-
}
14
-
]
15
-
},
16
-
"type": {
17
-
"multiple": false,
18
-
"required": true,
19
-
"types": [
20
-
{
21
-
"type": "type",
22
-
"named": true
23
-
}
24
-
]
25
-
}
26
-
}
27
-
},
28
-
{
29
3
"type": "array_literal",
30
4
"named": true,
31
5
"fields": {},
···
156
130
}
157
131
},
158
132
{
133
+
"type": "def_type_definition",
134
+
"named": true,
135
+
"fields": {
136
+
"name": {
137
+
"multiple": false,
138
+
"required": true,
139
+
"types": [
140
+
{
141
+
"type": "identifier",
142
+
"named": true
143
+
}
144
+
]
145
+
},
146
+
"type": {
147
+
"multiple": false,
148
+
"required": true,
149
+
"types": [
150
+
{
151
+
"type": "type",
152
+
"named": true
153
+
}
154
+
]
155
+
}
156
+
}
157
+
},
158
+
{
159
159
"type": "error_definition",
160
160
"named": true,
161
161
"fields": {
···
223
223
"fields": {}
224
224
},
225
225
{
226
+
"type": "inline_type_definition",
227
+
"named": true,
228
+
"fields": {
229
+
"name": {
230
+
"multiple": false,
231
+
"required": true,
232
+
"types": [
233
+
{
234
+
"type": "identifier",
235
+
"named": true
236
+
}
237
+
]
238
+
},
239
+
"type": {
240
+
"multiple": false,
241
+
"required": true,
242
+
"types": [
243
+
{
244
+
"type": "type",
245
+
"named": true
246
+
}
247
+
]
248
+
}
249
+
}
250
+
},
251
+
{
226
252
"type": "item",
227
253
"named": true,
228
254
"fields": {},
···
231
257
"required": true,
232
258
"types": [
233
259
{
234
-
"type": "alias_definition",
260
+
"type": "def_type_definition",
261
+
"named": true
262
+
},
263
+
{
264
+
"type": "inline_type_definition",
235
265
"named": true
236
266
},
237
267
{
···
701
731
"named": false
702
732
},
703
733
{
704
-
"type": "alias",
705
-
"named": false
706
-
},
707
-
{
708
734
"type": "blob",
709
735
"named": false
710
736
},
···
725
751
"named": false
726
752
},
727
753
{
754
+
"type": "def",
755
+
"named": false
756
+
},
757
+
{
728
758
"type": "doc_comment",
729
759
"named": true
730
760
},
···
733
763
"named": false
734
764
},
735
765
{
766
+
"type": "inline",
767
+
"named": false
768
+
},
769
+
{
736
770
"type": "integer",
737
771
"named": false
738
772
},
···
770
804
},
771
805
{
772
806
"type": "string",
773
-
"named": true
807
+
"named": false
774
808
},
775
809
{
776
810
"type": "string",
777
-
"named": false
811
+
"named": true
778
812
},
779
813
{
780
814
"type": "subscription",
···
790
824
},
791
825
{
792
826
"type": "true",
827
+
"named": false
828
+
},
829
+
{
830
+
"type": "type",
793
831
"named": false
794
832
},
795
833
{
+173
-130
website/content/docs/syntax.md
+173
-130
website/content/docs/syntax.md
···
15
15
```
16
16
17
17
### File Naming Convention
18
-
Files should follow the lexicon NSID:
18
+
The file path determines the lexicon NSID. Files should follow the lexicon NSID structure:
19
19
- `com.example.forum.thread.mlf` → Lexicon NSID: `com.example.forum.thread`
20
20
- `com.example.user.profile.mlf` → Lexicon NSID: `com.example.user.profile`
21
+
22
+
The lexicon NSID is derived solely from the filename, not from any internal declarations.
21
23
22
24
## Basic Structure
23
25
24
26
Every MLF file can contain:
25
27
26
-
- Namespace declarations
27
28
- Use statements (imports)
28
-
- Type definitions (record, alias, token, query, procedure, subscription)
29
+
- Type definitions (record, inline type, def type, token, query, procedure, subscription)
30
+
31
+
## Syntax Rules
32
+
33
+
### Semicolons
34
+
35
+
- **Records** do NOT have semicolons after the closing brace `}`
36
+
- All other definitions require semicolons:
37
+
- `use` statements end with `;`
38
+
- `token` definitions end with `;`
39
+
- `inline type` definitions end with `;`
40
+
- `def type` definitions end with `;`
41
+
- `query` definitions end with `;`
42
+
- `procedure` definitions end with `;`
43
+
- `subscription` definitions end with `;`
44
+
45
+
### Commas
46
+
47
+
Commas are **required** between items, with **trailing commas allowed**:
48
+
49
+
**Record fields:**
50
+
```mlf
51
+
record example {
52
+
field1: string,
53
+
field2: integer, // trailing comma allowed
54
+
}
55
+
```
56
+
57
+
**Constraints:**
58
+
```mlf
59
+
title: string constrained {
60
+
maxLength: 200,
61
+
minLength: 1, // trailing comma allowed
62
+
}
63
+
```
64
+
65
+
**Error definitions:**
66
+
```mlf
67
+
query getThread(): thread | error {
68
+
NotFound,
69
+
BadRequest, // trailing comma allowed
70
+
}
71
+
```
29
72
30
73
## Primitive Types
31
74
···
63
106
record thread {
64
107
/// Thread title
65
108
title: string constrained {
66
-
maxLength: 200,
67
-
minLength: 1,
68
-
},
109
+
maxLength: 200
110
+
minLength: 1
111
+
}
69
112
/// Thread body
70
-
body?: string, // Optional field
113
+
body?: string // Optional field
71
114
/// Thread creation timestamp
72
-
createdAt: Datetime,
115
+
createdAt: Datetime
116
+
}
117
+
```
118
+
119
+
## Type Definitions
120
+
121
+
MLF supports two kinds of type definitions:
122
+
123
+
### Inline Types
124
+
125
+
Expanded at the point of use, never appear in generated lexicon defs:
126
+
127
+
```mlf
128
+
inline type AtIdentifier = string constrained {
129
+
format "at-identifier"
73
130
};
74
131
```
75
132
76
-
## Aliases
133
+
### Def Types
77
134
78
-
Type aliases define reusable object shapes:
135
+
Become named definitions in the lexicon's defs block:
79
136
80
137
```mlf
81
-
alias replyRef = {
82
-
root: AtUri,
83
-
parent: AtUri,
138
+
def type replyRef = {
139
+
root: AtUri
140
+
parent: AtUri
84
141
};
85
142
86
143
record thread {
87
-
reply?: replyRef,
88
-
};
144
+
reply?: replyRef
145
+
}
89
146
```
90
147
91
-
If used in multiple places, they will be hoisted to a def. If only used once, they will be inlined.
148
+
Use `inline type` for type aliases that should be expanded inline (like primitive type wrappers). Use `def type` for types that should be referenced by name in the generated lexicon.
92
149
93
150
## Tokens
94
151
···
103
160
104
161
record issue {
105
162
state: string constrained {
106
-
knownValues: [open, closed],
107
-
default: "open",
108
-
},
109
-
};
163
+
knownValues: [open, closed]
164
+
default: "open"
165
+
}
166
+
}
110
167
```
111
168
112
169
Tokens must have doc comments describing their purpose.
···
117
174
118
175
```mlf
119
176
title: string constrained {
120
-
maxLength: 200,
121
-
minLength: 1,
122
-
};
177
+
maxLength: 200
178
+
minLength: 1
179
+
}
123
180
124
181
age: integer constrained {
125
-
minimum: 0,
126
-
maximum: 150,
127
-
};
182
+
minimum: 0
183
+
maximum: 150
184
+
}
128
185
129
186
status: string constrained {
130
-
enum: ["draft", "published", "archived"],
131
-
};
187
+
enum: ["draft", "published", "archived"]
188
+
}
132
189
```
133
190
134
191
### String Constraints
···
136
193
- `maxLength` / `minLength` - Length in bytes
137
194
- `maxGraphemes` / `minGraphemes` - Length in grapheme clusters
138
195
- `format` - Format validation (datetime, uri, did, handle, etc.)
139
-
- `enum` - Allowed values (closed set)
140
-
- `knownValues` - Known values (extensible set, can reference tokens)
196
+
- `enum` - Allowed values (closed set) - accepts string literals or token references
197
+
- `knownValues` - Known values (extensible set) - accepts string literals or token references
141
198
- `default` - Default value
142
199
200
+
**enum, knownValues, and default** can use either literals or references:
201
+
```mlf
202
+
// String literals
203
+
status: string constrained {
204
+
knownValues: ["open", "closed", "pending"]
205
+
default: "open"
206
+
}
207
+
208
+
// References to named items (tokens, aliases, records, etc.)
209
+
token open;
210
+
token closed;
211
+
212
+
status: string constrained {
213
+
knownValues: [open, closed] // References tokens defined above
214
+
default: open // References the token
215
+
}
216
+
```
217
+
143
218
### Integer Constraints
144
219
145
220
- `minimum` / `maximum` - Min/max values
···
150
225
151
226
```mlf
152
227
tags: string[] constrained {
153
-
minLength: 1,
154
-
maxLength: 10,
228
+
minLength: 1
229
+
maxLength: 10
155
230
}
156
231
```
157
232
···
159
234
160
235
```mlf
161
236
avatar: blob constrained {
162
-
accept: ["image/png", "image/jpeg"],
163
-
maxSize: 1000000, // bytes
237
+
accept: ["image/png", "image/jpeg"]
238
+
maxSize: 1000000 // bytes
164
239
}
165
240
```
166
241
···
168
243
169
244
```mlf
170
245
field: boolean constrained {
171
-
default: false,
246
+
default: false
172
247
}
173
248
```
174
249
···
177
252
Constraints can only make types **more restrictive**, never less restrictive:
178
253
179
254
```mlf
180
-
alias shortString = string constrained {
181
-
maxLength: 100,
255
+
def type shortString = string constrained {
256
+
maxLength: 100
182
257
};
183
258
184
259
record post {
185
260
// Valid: 50 is more restrictive than 100
186
261
title: shortString constrained {
187
-
maxLength: 50,
188
-
},
189
-
};
262
+
maxLength: 50
263
+
}
264
+
}
190
265
```
191
266
192
267
**Refinement rules:**
···
231
306
232
307
```mlf
233
308
metadata: {
234
-
version: integer,
235
-
timestamp: Datetime,
309
+
version: integer
310
+
timestamp: Datetime
236
311
}
237
312
```
238
313
···
244
319
/// Get a user profile
245
320
query getProfile(
246
321
/// The actor's DID or handle
247
-
actor: AtIdentifier,
322
+
actor: AtIdentifier
248
323
): profile;
249
324
```
250
325
···
252
327
253
328
```mlf
254
329
query getThread(
255
-
uri: AtUri,
330
+
uri: AtUri
256
331
): thread | error {
257
332
/// Thread not found
258
-
NotFound,
333
+
NotFound
259
334
/// Invalid request
260
-
BadRequest,
335
+
BadRequest
261
336
};
262
337
```
263
338
···
268
343
```mlf
269
344
/// Create a new thread
270
345
procedure createThread(
271
-
title: string,
272
-
body: string,
346
+
title: string
347
+
body: string
273
348
): {
274
-
uri: AtUri,
275
-
cid: Cid,
349
+
uri: AtUri
350
+
cid: Cid
276
351
} | error {
277
352
/// Title too long
278
-
TitleTooLong,
353
+
TitleTooLong
279
354
};
280
355
```
281
356
···
287
362
/// Subscribe to repository events
288
363
subscription subscribeRepos(
289
364
/// Optional cursor for resuming
290
-
cursor?: integer,
365
+
cursor?: integer
291
366
): commit | identity | handle;
292
367
```
293
368
294
-
Message types must be defined as aliases or records:
369
+
Message types must be defined as def types or records:
295
370
296
371
```mlf
297
372
/// Commit message
298
-
alias commit = {
299
-
seq: integer,
300
-
repo: Did,
301
-
commit: Cid,
302
-
time: Datetime,
373
+
def type commit = {
374
+
seq: integer
375
+
repo: Did
376
+
commit: Cid
377
+
time: Datetime
303
378
};
304
379
305
380
/// Identity message
306
-
alias identity = {
307
-
did: Did,
308
-
handle: Handle,
381
+
def type identity = {
382
+
did: Did
383
+
handle: Handle
309
384
};
310
385
```
311
386
···
319
394
/// A forum thread
320
395
record thread {
321
396
/// Thread title
322
-
title: string,
323
-
};
397
+
title: string
398
+
}
324
399
```
325
400
326
401
### Regular Comments
···
330
405
```mlf
331
406
// This is a regular comment
332
407
record example {
333
-
field: string, // inline comment
334
-
};
408
+
field: string // inline comment
409
+
}
335
410
```
336
411
337
412
## Annotations
···
342
417
```mlf
343
418
@deprecated
344
419
record oldRecord {
345
-
field: string,
420
+
field: string
346
421
}
347
422
```
348
423
···
351
426
@since(1, 2, 0)
352
427
@doc("https://example.com/docs")
353
428
record example {
354
-
field: string,
429
+
field: string
355
430
}
356
431
```
357
432
···
361
436
@table(name: "threads", indexes: "did,createdAt")
362
437
record thread {
363
438
@indexed
364
-
did: Did,
439
+
did: Did
365
440
366
441
@sensitive(pii: true)
367
-
title: string,
442
+
title: string
368
443
}
369
444
```
370
445
371
-
Annotations can be placed on records, aliases, tokens, queries, procedures, subscriptions, and fields.
446
+
Annotations can be placed on records, inline types, def types, tokens, queries, procedures, subscriptions, and fields.
372
447
373
448
## Imports
374
449
···
397
472
use com.example.user.profile;
398
473
399
474
record thread {
400
-
author: profile, // Instead of com.example.user.profile
401
-
}
402
-
```
403
-
404
-
## Namespaces
405
-
406
-
Organize related definitions:
407
-
408
-
```mlf
409
-
namespace com.example.forum.thread;
410
-
411
-
record thread {
412
-
title: string,
413
-
};
414
-
```
415
-
416
-
Or use nested namespaces:
417
-
418
-
```mlf
419
-
namespace .forum {
420
-
record thread {
421
-
title: string,
422
-
}
423
-
424
-
query getThread(
425
-
uri: AtUri,
426
-
): thread;
427
-
}
428
-
429
-
namespace .user {
430
-
record profile {
431
-
displayName: string,
432
-
}
475
+
author: profile // Instead of com.example.user.profile
433
476
}
434
477
```
435
478
···
440
483
```mlf
441
484
// Local reference (same file)
442
485
record thread {
443
-
author: author, // References 'alias author' in same file
486
+
author: author // References 'def type author' in same file
444
487
}
445
488
446
489
// Cross-file reference
447
490
record thread {
448
-
profile: com.example.user.profile, // References com/example/user/profile.mlf
491
+
profile: com.example.user.profile // References com/example/user/profile.mlf
449
492
}
450
493
```
451
494
452
-
**Note:** All references use dotted notation. The `#` character is NOT used for references.
495
+
All references use dotted notation.
453
496
454
497
## Optional Fields
455
498
···
457
500
458
501
```mlf
459
502
record thread {
460
-
title: string, // Required
461
-
body?: string, // Optional
462
-
tags?: string[], // Optional array
503
+
title: string // Required
504
+
body?: string // Optional
505
+
tags?: string[] // Optional array
463
506
}
464
507
```
465
508
···
468
511
Use backticks to escape reserved keywords when you need to use them as identifiers:
469
512
470
513
```mlf
471
-
alias `record` = {
472
-
`record`: com.atproto.repo.strongRef,
473
-
`error`: string,
514
+
def type `record` = {
515
+
`record`: com.atproto.repo.strongRef
516
+
`error`: string
474
517
};
475
518
```
476
519
···
509
552
record thread {
510
553
/// Thread title
511
554
title: string constrained {
512
-
minGraphemes: 1,
513
-
maxGraphemes: 200,
514
-
},
555
+
minGraphemes: 1
556
+
maxGraphemes: 200
557
+
}
515
558
/// Thread body (markdown)
516
559
body?: string constrained {
517
-
maxGraphemes: 10000,
518
-
},
560
+
maxGraphemes: 10000
561
+
}
519
562
/// Thread state
520
563
state: string constrained {
521
-
knownValues: [open, closed],
522
-
default: "open",
523
-
},
564
+
knownValues: [open, closed]
565
+
default: "open"
566
+
}
524
567
/// Author profile
525
-
author: profile,
568
+
author: profile
526
569
/// Creation timestamp
527
-
createdAt: Datetime,
528
-
};
570
+
createdAt: Datetime
571
+
}
529
572
530
573
/// Get a thread by URI
531
574
query getThread(
532
575
/// Thread AT-URI
533
-
uri: AtUri,
576
+
uri: AtUri
534
577
): thread | error {
535
578
/// Thread not found
536
-
NotFound,
579
+
NotFound
537
580
};
538
581
539
582
/// Create a new thread
540
583
procedure createThread(
541
-
title: string,
542
-
body?: string,
584
+
title: string
585
+
body?: string
543
586
): {
544
-
uri: AtUri,
545
-
cid: Cid,
587
+
uri: AtUri
588
+
cid: Cid
546
589
} | error {
547
590
/// Title too long
548
-
TitleTooLong,
591
+
TitleTooLong
549
592
};
550
593
```
+115
-15
website/sass/style.scss
+115
-15
website/sass/style.scss
···
381
381
padding: 0.75rem 1rem;
382
382
background: var(--bg-alt);
383
383
border-bottom: 1px solid var(--border);
384
-
height: 3rem;
384
+
min-height: 3rem;
385
+
flex-wrap: wrap;
386
+
gap: 0.5rem;
385
387
}
386
388
387
389
.panel-header h3 {
···
390
392
margin: 0;
391
393
}
392
394
395
+
.file-path-container {
396
+
display: flex;
397
+
align-items: center;
398
+
flex: 1;
399
+
}
400
+
401
+
.file-path-container input {
402
+
flex: 1;
403
+
padding: 0.375rem 0.75rem;
404
+
background: var(--bg);
405
+
border: 1px solid var(--border);
406
+
border-radius: 0.25rem;
407
+
color: var(--text);
408
+
font-size: 0.875rem;
409
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
410
+
min-width: 0;
411
+
}
412
+
413
+
.file-path-container input:focus {
414
+
outline: none;
415
+
border-color: var(--accent);
416
+
}
417
+
393
418
.tabs {
394
419
display: flex;
395
420
gap: 0.5rem;
···
443
468
color: #718096;
444
469
}
445
470
446
-
.shiki-editor-container {
471
+
.editor-container-with-lines {
447
472
width: 100%;
448
473
flex: 1;
449
474
min-height: 0;
450
-
overflow: auto;
451
475
background: var(--code-bg);
476
+
display: flex;
477
+
flex-direction: row;
478
+
}
479
+
480
+
.editor-wrapper {
481
+
position: relative;
482
+
flex: 1;
483
+
min-height: 0;
484
+
overflow: hidden;
452
485
}
453
486
454
-
.shiki-editor {
455
-
min-height: 100%;
487
+
.highlight-backdrop {
488
+
position: absolute;
489
+
top: 0;
490
+
left: 0;
491
+
right: 0;
492
+
bottom: 0;
493
+
padding: 1rem;
494
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace;
495
+
font-size: 0.875rem;
496
+
line-height: 1.6;
497
+
white-space: pre;
498
+
overflow: hidden;
499
+
pointer-events: none;
500
+
z-index: 1;
501
+
}
502
+
503
+
.highlight-backdrop span {
504
+
font-family: inherit;
505
+
font-size: inherit;
506
+
line-height: inherit;
507
+
}
508
+
509
+
.mlf-textarea {
510
+
position: absolute;
511
+
top: 0;
512
+
left: 0;
513
+
right: 0;
514
+
bottom: 0;
515
+
width: 100%;
516
+
height: 100%;
456
517
padding: 1rem;
457
518
border: none;
458
-
font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace;
519
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace;
459
520
font-size: 0.875rem;
460
521
line-height: 1.6;
461
-
color: var(--code-text);
522
+
resize: none;
523
+
background: transparent;
524
+
color: transparent;
462
525
white-space: pre;
463
526
tab-size: 4;
464
-
caret-color: var(--text);
527
+
overflow: auto;
528
+
z-index: 2;
529
+
caret-color: #fff;
530
+
-webkit-text-fill-color: transparent;
465
531
}
466
532
467
-
.shiki-editor:focus {
533
+
.mlf-textarea:focus {
468
534
outline: none;
469
535
}
470
536
471
-
.shiki-editor span {
472
-
font-family: inherit;
473
-
font-size: inherit;
474
-
line-height: inherit;
537
+
.mlf-textarea::selection {
538
+
background: rgba(255, 255, 255, 0.2);
475
539
}
476
540
477
-
.shiki-output-container {
541
+
.shiki-output-outer-container {
478
542
width: 100%;
479
543
flex: 1;
480
544
min-height: 0;
481
-
overflow: auto;
482
545
background: var(--code-bg);
546
+
display: flex;
547
+
flex-direction: row;
548
+
}
549
+
550
+
.shiki-output-container {
551
+
min-height: 100%;
483
552
padding: 1rem;
484
553
font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace;
485
554
font-size: 0.875rem;
···
836
905
color: var(--accent);
837
906
text-decoration: none;
838
907
}
908
+
909
+
/* Line numbers for code editor */
910
+
.line-numbers {
911
+
flex-shrink: 0;
912
+
min-width: 3rem;
913
+
padding: 1rem 0.5rem 1rem 1rem;
914
+
text-align: right;
915
+
user-select: none;
916
+
color: var(--text-muted);
917
+
background: var(--code-bg);
918
+
border-right: 1px solid var(--border);
919
+
overflow: hidden;
920
+
}
921
+
922
+
.line-number {
923
+
font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace;
924
+
font-size: 0.875rem;
925
+
line-height: 1.6;
926
+
}
927
+
928
+
.editor-wrapper {
929
+
flex: 1;
930
+
overflow: auto;
931
+
min-width: 0;
932
+
}
933
+
934
+
.output-wrapper {
935
+
flex: 1;
936
+
overflow: auto;
937
+
min-width: 0;
938
+
}
+161
-43
website/static/js/app.js
+161
-43
website/static/js/app.js
···
35
35
patterns: [
36
36
{
37
37
name: 'keyword.control.mlf',
38
-
match: '\\b(namespace|use|record|alias|token|query|procedure|subscription|throws|constrained)\\b'
38
+
match: '\\b(use|record|inline|def|type|token|query|procedure|subscription|throws|constrained)\\b'
39
39
},
40
40
{
41
41
name: 'keyword.other.mlf',
42
-
match: '\\b(main)\\b'
42
+
match: '\\b(main|defs)\\b'
43
43
}
44
44
]
45
45
},
···
144
144
const textarea = document.getElementById('mlf-editor');
145
145
const initialCode = textarea.value;
146
146
147
-
// Create editor container
147
+
// Create editor container with line numbers and highlighting
148
148
editorContainer = document.createElement('div');
149
-
editorContainer.className = 'shiki-editor-container';
149
+
editorContainer.className = 'editor-container-with-lines';
150
150
151
-
const editor = document.createElement('div');
152
-
editor.className = 'shiki-editor';
153
-
editor.contentEditable = 'true';
154
-
editor.spellcheck = false;
155
-
editor.id = 'shiki-editor';
151
+
// Create line numbers
152
+
const lineNumbers = document.createElement('div');
153
+
lineNumbers.className = 'line-numbers';
154
+
lineNumbers.id = 'line-numbers';
155
+
156
+
// Create wrapper for textarea and highlight layer
157
+
const editorWrapper = document.createElement('div');
158
+
editorWrapper.className = 'editor-wrapper';
159
+
160
+
// Create highlight backdrop
161
+
const highlightBackdrop = document.createElement('div');
162
+
highlightBackdrop.className = 'highlight-backdrop';
163
+
highlightBackdrop.id = 'highlight-backdrop';
164
+
165
+
// Style the textarea
166
+
textarea.className = 'mlf-textarea';
167
+
168
+
// Insert container before textarea, then build the structure
169
+
const parent = textarea.parentNode;
170
+
parent.insertBefore(editorContainer, textarea);
156
171
157
-
editorContainer.appendChild(editor);
172
+
editorWrapper.appendChild(highlightBackdrop);
173
+
editorWrapper.appendChild(textarea);
158
174
159
-
// Replace textarea with editor
160
-
textarea.style.display = 'none';
161
-
textarea.parentNode.insertBefore(editorContainer, textarea);
175
+
editorContainer.appendChild(lineNumbers);
176
+
editorContainer.appendChild(editorWrapper);
162
177
163
-
// Set initial content
178
+
// Set initial line numbers and highlighting
179
+
updateLineNumbers(initialCode);
164
180
updateHighlighting(initialCode);
165
181
166
-
// Convert JSON output textarea to highlighted div
182
+
// Convert JSON output textarea to highlighted div with line numbers
167
183
const jsonTextarea = document.getElementById('lexicon-result');
184
+
185
+
const jsonOuterContainer = document.createElement('div');
186
+
jsonOuterContainer.className = 'shiki-output-outer-container';
187
+
jsonOuterContainer.id = 'lexicon-result-outer-container';
188
+
189
+
const jsonLineNumbers = document.createElement('div');
190
+
jsonLineNumbers.className = 'line-numbers';
191
+
jsonLineNumbers.id = 'json-line-numbers';
192
+
193
+
const jsonWrapper = document.createElement('div');
194
+
jsonWrapper.className = 'output-wrapper';
195
+
168
196
const jsonContainer = document.createElement('div');
169
197
jsonContainer.className = 'shiki-output-container';
170
198
jsonContainer.id = 'lexicon-result-container';
171
199
200
+
jsonWrapper.appendChild(jsonContainer);
201
+
jsonOuterContainer.appendChild(jsonLineNumbers);
202
+
jsonOuterContainer.appendChild(jsonWrapper);
203
+
172
204
jsonTextarea.style.display = 'none';
173
-
jsonTextarea.parentNode.insertBefore(jsonContainer, jsonTextarea);
205
+
jsonTextarea.parentNode.insertBefore(jsonOuterContainer, jsonTextarea);
174
206
}
175
207
176
208
function updateHighlighting(code) {
177
-
if (!highlighter || !editorContainer) return;
209
+
if (!highlighter) return;
178
210
179
-
const editor = editorContainer.querySelector('.shiki-editor');
180
-
if (!editor) return;
211
+
const backdrop = document.getElementById('highlight-backdrop');
212
+
if (!backdrop) return;
181
213
182
-
// Store cursor position
183
-
const cursorOffset = getCaretPosition(editor);
184
-
185
-
// Update highlighted content
214
+
// Update highlighted content in backdrop
186
215
const html = highlighter.codeToHtml(code, {
187
216
lang: 'mlf',
188
217
theme: 'dracula'
···
194
223
const codeElement = temp.querySelector('code');
195
224
196
225
if (codeElement) {
197
-
editor.innerHTML = codeElement.innerHTML;
226
+
backdrop.innerHTML = codeElement.innerHTML;
198
227
} else {
199
-
editor.textContent = code;
228
+
backdrop.textContent = code;
200
229
}
230
+
}
201
231
202
-
// Restore cursor position
203
-
if (document.activeElement === editor) {
204
-
setCaretPosition(editor, cursorOffset);
205
-
}
232
+
function updateLineNumbers(code) {
233
+
const lineNumbers = document.getElementById('line-numbers');
234
+
if (!lineNumbers) return;
235
+
236
+
const lines = code.split('\n').length;
237
+
const lineNumbersHtml = Array.from({ length: lines }, (_, i) =>
238
+
`<div class="line-number">${i + 1}</div>`
239
+
).join('');
240
+
241
+
lineNumbers.innerHTML = lineNumbersHtml;
206
242
}
207
243
208
244
function getCaretPosition(element) {
···
256
292
}
257
293
258
294
function getEditorContent() {
259
-
const editor = editorContainer?.querySelector('.shiki-editor');
260
-
return editor ? editor.textContent : '';
295
+
const textarea = document.getElementById('mlf-editor');
296
+
return textarea ? textarea.value : '';
261
297
}
262
298
263
299
function updateJsonOutput(jsonString) {
···
284
320
} else {
285
321
container.textContent = formatted;
286
322
}
323
+
324
+
// Update line numbers for JSON output
325
+
updateJsonLineNumbers(formatted);
287
326
} catch (e) {
288
327
container.textContent = jsonString;
328
+
updateJsonLineNumbers(jsonString);
289
329
}
290
330
}
291
331
332
+
function updateJsonLineNumbers(code) {
333
+
const lineNumbers = document.getElementById('json-line-numbers');
334
+
if (!lineNumbers) return;
335
+
336
+
const lines = code.split('\n').length;
337
+
const lineNumbersHtml = Array.from({ length: lines }, (_, i) =>
338
+
`<div class="line-number">${i + 1}</div>`
339
+
).join('');
340
+
341
+
lineNumbers.innerHTML = lineNumbersHtml;
342
+
}
343
+
292
344
function hidePlayground() {
293
345
const playground = document.querySelector('.playground-container');
294
346
if (playground) {
···
307
359
});
308
360
309
361
// Editor input with debounce
310
-
const editor = editorContainer?.querySelector('.shiki-editor');
311
-
if (editor) {
312
-
let timeout;
313
-
editor.addEventListener('input', () => {
314
-
clearTimeout(timeout);
315
-
timeout = setTimeout(() => {
316
-
const code = getEditorContent();
317
-
updateHighlighting(code);
362
+
const textarea = document.getElementById('mlf-editor');
363
+
if (textarea) {
364
+
let checkTimeout;
365
+
366
+
textarea.addEventListener('input', () => {
367
+
const code = textarea.value;
368
+
369
+
// Update line numbers and highlighting immediately
370
+
updateLineNumbers(code);
371
+
updateHighlighting(code);
372
+
373
+
// Clear previous timeout
374
+
clearTimeout(checkTimeout);
375
+
376
+
// Debounce validation/check only
377
+
checkTimeout = setTimeout(() => {
318
378
handleCheck();
319
379
}, 500);
320
380
});
381
+
382
+
// Synchronize scroll between textarea, line numbers, and backdrop
383
+
const lineNumbers = document.getElementById('line-numbers');
384
+
const backdrop = document.getElementById('highlight-backdrop');
385
+
if (lineNumbers && backdrop) {
386
+
textarea.addEventListener('scroll', () => {
387
+
lineNumbers.scrollTop = textarea.scrollTop;
388
+
// Use transform to scroll the backdrop in sync
389
+
backdrop.style.transform = `translate(-${textarea.scrollLeft}px, -${textarea.scrollTop}px)`;
390
+
});
391
+
}
392
+
}
393
+
394
+
// Synchronize scroll between JSON output and line numbers
395
+
const jsonWrapper = document.querySelector('.output-wrapper');
396
+
const jsonLineNumbers = document.getElementById('json-line-numbers');
397
+
if (jsonWrapper && jsonLineNumbers) {
398
+
jsonWrapper.addEventListener('scroll', () => {
399
+
jsonLineNumbers.scrollTop = jsonWrapper.scrollTop;
400
+
});
321
401
}
322
402
323
403
// Auto-validate on record input change (debounced)
···
329
409
timeout = setTimeout(handleValidate, 500);
330
410
});
331
411
}
412
+
413
+
// File path input change triggers re-generation
414
+
const filePathInput = document.getElementById('file-path');
415
+
if (filePathInput) {
416
+
let timeout;
417
+
filePathInput.addEventListener('input', () => {
418
+
clearTimeout(timeout);
419
+
timeout = setTimeout(handleCheck, 500);
420
+
});
421
+
}
332
422
}
333
423
334
424
function switchTab(tabName) {
···
343
433
});
344
434
}
345
435
436
+
function extractNamespaceFromPath(filePath) {
437
+
// Remove leading/trailing slashes
438
+
filePath = filePath.trim().replace(/^\/+|\/+$/g, '');
439
+
440
+
// Validate it ends with .mlf
441
+
if (!filePath.endsWith('.mlf')) {
442
+
return null;
443
+
}
444
+
445
+
// Remove the .mlf extension
446
+
const withoutExt = filePath.slice(0, -4);
447
+
448
+
// Replace slashes with dots to get the namespace
449
+
const namespace = withoutExt.replace(/\//g, '.');
450
+
451
+
// Validate namespace format (should be valid NSID segments)
452
+
// Basic validation: only alphanumeric, dots, and hyphens
453
+
if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(namespace)) {
454
+
return null;
455
+
}
456
+
457
+
return namespace;
458
+
}
459
+
346
460
function handleCheck() {
347
461
if (!wasm) {
348
462
return;
349
463
}
350
464
351
465
const source = getEditorContent();
466
+
const filePath = document.getElementById('file-path').value;
467
+
468
+
// Extract namespace from file path
469
+
const namespace = extractNamespaceFromPath(filePath);
470
+
if (!namespace) {
471
+
showError('Invalid file path. Must be a valid path ending in .mlf (e.g., com/example/app/thread.mlf)');
472
+
return;
473
+
}
352
474
353
475
try {
354
476
// Check the MLF source
···
356
478
357
479
if (checkResult.success) {
358
480
hideError();
359
-
360
-
// Generate lexicon - extract namespace from source or use default
361
-
const namespaceMatch = source.match(/namespace\s+([\w.]+)/);
362
-
const namespace = namespaceMatch ? namespaceMatch[1] : 'com.example.post';
363
481
364
482
const generateResult = wasm.generate_lexicon(source, namespace);
365
483
+2
-2
website/syntaxes/mlf.sublime-syntax
+2
-2
website/syntaxes/mlf.sublime-syntax
···
29
29
pop: true
30
30
31
31
keywords:
32
-
- match: '\b(namespace|use|record|alias|token|query|procedure|subscription|throws|constrained)\b'
32
+
- match: '\b(namespace|use|record|inline|def|type|token|query|procedure|subscription|throws|constrained)\b'
33
33
scope: keyword.control.mlf
34
-
- match: '\b(main)\b'
34
+
- match: '\b(main|defs)\b'
35
35
scope: keyword.other.mlf
36
36
37
37
types:
+5
-4
website/templates/playground.html
+5
-4
website/templates/playground.html
···
5
5
<div class="playground-container">
6
6
<div class="editor-panel">
7
7
<div class="panel-header">
8
-
<h3>MLF Source</h3>
8
+
<div class="file-path-container">
9
+
<input type="text" id="file-path" value="com/example/forum/thread.mlf" spellcheck="false" />
10
+
</div>
9
11
</div>
10
12
<textarea id="mlf-editor" spellcheck="false">/// A forum thread
11
13
record thread {
···
15
17
minLength: 1,
16
18
},
17
19
/// Thread body
18
-
body: string constrained {
20
+
body?: string constrained {
19
21
maxLength: 10000,
20
22
},
21
23
/// Thread creation timestamp
22
24
createdAt: Datetime,
23
-
};</textarea>
25
+
}</textarea>
24
26
</div>
25
27
26
28
<div class="output-panel">
27
29
<div class="panel-header">
28
-
<h3>Output</h3>
29
30
<div class="tabs">
30
31
<button class="tab active" data-tab="lexicon">Lexicon</button>
31
32
<button class="tab" data-tab="validate">Validate</button>