A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Init

Matt Stavola 0aadec64

+12768
+18
.gitignore
··· 1 + # Rust build artifacts 2 + /target 3 + Cargo.lock 4 + 5 + # IDE/Editor 6 + .vscode/ 7 + .idea/ 8 + *.swp 9 + *.swo 10 + *~ 11 + 12 + # OS 13 + .DS_Store 14 + Thumbs.db 15 + 16 + # Claude Code 17 + CLAUDE.md 18 + .claude/*.local.json
+885
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.25.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.1" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "anstream" 31 + version = "0.6.21" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 34 + dependencies = [ 35 + "anstyle", 36 + "anstyle-parse", 37 + "anstyle-query", 38 + "anstyle-wincon", 39 + "colorchoice", 40 + "is_terminal_polyfill", 41 + "utf8parse", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle" 46 + version = "1.0.13" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 49 + 50 + [[package]] 51 + name = "anstyle-parse" 52 + version = "0.2.7" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 55 + dependencies = [ 56 + "utf8parse", 57 + ] 58 + 59 + [[package]] 60 + name = "anstyle-query" 61 + version = "1.1.4" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 64 + dependencies = [ 65 + "windows-sys", 66 + ] 67 + 68 + [[package]] 69 + name = "anstyle-wincon" 70 + version = "3.0.10" 71 + source = "registry+https://github.com/rust-lang/crates.io-index" 72 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 73 + dependencies = [ 74 + "anstyle", 75 + "once_cell_polyfill", 76 + "windows-sys", 77 + ] 78 + 79 + [[package]] 80 + name = "backtrace" 81 + version = "0.3.76" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 84 + dependencies = [ 85 + "addr2line", 86 + "cfg-if", 87 + "libc", 88 + "miniz_oxide", 89 + "object", 90 + "rustc-demangle", 91 + "windows-link", 92 + ] 93 + 94 + [[package]] 95 + name = "backtrace-ext" 96 + version = "0.2.1" 97 + source = "registry+https://github.com/rust-lang/crates.io-index" 98 + checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 99 + dependencies = [ 100 + "backtrace", 101 + ] 102 + 103 + [[package]] 104 + name = "bitflags" 105 + version = "2.9.4" 106 + source = "registry+https://github.com/rust-lang/crates.io-index" 107 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 108 + 109 + [[package]] 110 + name = "bumpalo" 111 + version = "3.19.0" 112 + source = "registry+https://github.com/rust-lang/crates.io-index" 113 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 114 + 115 + [[package]] 116 + name = "cc" 117 + version = "1.2.40" 118 + source = "registry+https://github.com/rust-lang/crates.io-index" 119 + checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" 120 + dependencies = [ 121 + "find-msvc-tools", 122 + "shlex", 123 + ] 124 + 125 + [[package]] 126 + name = "cfg-if" 127 + version = "1.0.3" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 130 + 131 + [[package]] 132 + name = "clap" 133 + version = "4.5.48" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" 136 + dependencies = [ 137 + "clap_builder", 138 + "clap_derive", 139 + ] 140 + 141 + [[package]] 142 + name = "clap_builder" 143 + version = "4.5.48" 144 + source = "registry+https://github.com/rust-lang/crates.io-index" 145 + checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" 146 + dependencies = [ 147 + "anstream", 148 + "anstyle", 149 + "clap_lex", 150 + "strsim", 151 + ] 152 + 153 + [[package]] 154 + name = "clap_derive" 155 + version = "4.5.47" 156 + source = "registry+https://github.com/rust-lang/crates.io-index" 157 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 158 + dependencies = [ 159 + "heck", 160 + "proc-macro2", 161 + "quote", 162 + "syn", 163 + ] 164 + 165 + [[package]] 166 + name = "clap_lex" 167 + version = "0.7.5" 168 + source = "registry+https://github.com/rust-lang/crates.io-index" 169 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 170 + 171 + [[package]] 172 + name = "colorchoice" 173 + version = "1.0.4" 174 + source = "registry+https://github.com/rust-lang/crates.io-index" 175 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 176 + 177 + [[package]] 178 + name = "errno" 179 + version = "0.3.14" 180 + source = "registry+https://github.com/rust-lang/crates.io-index" 181 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 182 + dependencies = [ 183 + "libc", 184 + "windows-sys", 185 + ] 186 + 187 + [[package]] 188 + name = "find-msvc-tools" 189 + version = "0.1.3" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" 192 + 193 + [[package]] 194 + name = "gimli" 195 + version = "0.32.3" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 198 + 199 + [[package]] 200 + name = "glob" 201 + version = "0.3.3" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 204 + 205 + [[package]] 206 + name = "heck" 207 + version = "0.5.0" 208 + source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 210 + 211 + [[package]] 212 + name = "is_ci" 213 + version = "1.2.0" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 216 + 217 + [[package]] 218 + name = "is_terminal_polyfill" 219 + version = "1.70.1" 220 + source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 222 + 223 + [[package]] 224 + name = "itoa" 225 + version = "1.0.15" 226 + source = "registry+https://github.com/rust-lang/crates.io-index" 227 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 228 + 229 + [[package]] 230 + name = "js-sys" 231 + version = "0.3.81" 232 + source = "registry+https://github.com/rust-lang/crates.io-index" 233 + checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 234 + dependencies = [ 235 + "once_cell", 236 + "wasm-bindgen", 237 + ] 238 + 239 + [[package]] 240 + name = "libc" 241 + version = "0.2.176" 242 + source = "registry+https://github.com/rust-lang/crates.io-index" 243 + checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 244 + 245 + [[package]] 246 + name = "linux-raw-sys" 247 + version = "0.11.0" 248 + source = "registry+https://github.com/rust-lang/crates.io-index" 249 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 250 + 251 + [[package]] 252 + name = "log" 253 + version = "0.4.28" 254 + source = "registry+https://github.com/rust-lang/crates.io-index" 255 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 256 + 257 + [[package]] 258 + name = "memchr" 259 + version = "2.7.6" 260 + source = "registry+https://github.com/rust-lang/crates.io-index" 261 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 262 + 263 + [[package]] 264 + name = "miette" 265 + version = "7.6.0" 266 + source = "registry+https://github.com/rust-lang/crates.io-index" 267 + checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 268 + dependencies = [ 269 + "backtrace", 270 + "backtrace-ext", 271 + "cfg-if", 272 + "miette-derive", 273 + "owo-colors", 274 + "supports-color", 275 + "supports-hyperlinks", 276 + "supports-unicode", 277 + "terminal_size", 278 + "textwrap", 279 + "unicode-width 0.1.14", 280 + ] 281 + 282 + [[package]] 283 + name = "miette-derive" 284 + version = "7.6.0" 285 + source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 287 + dependencies = [ 288 + "proc-macro2", 289 + "quote", 290 + "syn", 291 + ] 292 + 293 + [[package]] 294 + name = "minicov" 295 + version = "0.3.7" 296 + source = "registry+https://github.com/rust-lang/crates.io-index" 297 + checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" 298 + dependencies = [ 299 + "cc", 300 + "walkdir", 301 + ] 302 + 303 + [[package]] 304 + name = "miniz_oxide" 305 + version = "0.8.9" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 308 + dependencies = [ 309 + "adler2", 310 + ] 311 + 312 + [[package]] 313 + name = "mlf-cli" 314 + version = "0.1.0" 315 + dependencies = [ 316 + "clap", 317 + "glob", 318 + "miette", 319 + "mlf-codegen", 320 + "mlf-diagnostics", 321 + "mlf-lang", 322 + "mlf-validation", 323 + "serde", 324 + "serde_json", 325 + "thiserror", 326 + ] 327 + 328 + [[package]] 329 + name = "mlf-codegen" 330 + version = "0.1.0" 331 + dependencies = [ 332 + "mlf-lang", 333 + "serde_json", 334 + ] 335 + 336 + [[package]] 337 + name = "mlf-diagnostics" 338 + version = "0.1.0" 339 + dependencies = [ 340 + "miette", 341 + "mlf-lang", 342 + ] 343 + 344 + [[package]] 345 + name = "mlf-lang" 346 + version = "0.1.0" 347 + dependencies = [ 348 + "nom", 349 + ] 350 + 351 + [[package]] 352 + name = "mlf-validation" 353 + version = "0.1.0" 354 + dependencies = [ 355 + "mlf-lang", 356 + "serde_json", 357 + ] 358 + 359 + [[package]] 360 + name = "mlf-wasm" 361 + version = "0.1.0" 362 + dependencies = [ 363 + "mlf-codegen", 364 + "mlf-lang", 365 + "mlf-validation", 366 + "serde", 367 + "serde-wasm-bindgen", 368 + "serde_json", 369 + "wasm-bindgen", 370 + "wasm-bindgen-test", 371 + ] 372 + 373 + [[package]] 374 + name = "nom" 375 + version = "8.0.0" 376 + source = "registry+https://github.com/rust-lang/crates.io-index" 377 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 378 + dependencies = [ 379 + "memchr", 380 + ] 381 + 382 + [[package]] 383 + name = "object" 384 + version = "0.37.3" 385 + source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 387 + dependencies = [ 388 + "memchr", 389 + ] 390 + 391 + [[package]] 392 + name = "once_cell" 393 + version = "1.21.3" 394 + source = "registry+https://github.com/rust-lang/crates.io-index" 395 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 396 + 397 + [[package]] 398 + name = "once_cell_polyfill" 399 + version = "1.70.1" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 402 + 403 + [[package]] 404 + name = "owo-colors" 405 + version = "4.2.3" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 408 + 409 + [[package]] 410 + name = "proc-macro2" 411 + version = "1.0.101" 412 + source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 414 + dependencies = [ 415 + "unicode-ident", 416 + ] 417 + 418 + [[package]] 419 + name = "quote" 420 + version = "1.0.41" 421 + source = "registry+https://github.com/rust-lang/crates.io-index" 422 + checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 423 + dependencies = [ 424 + "proc-macro2", 425 + ] 426 + 427 + [[package]] 428 + name = "regex" 429 + version = "1.11.3" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" 432 + dependencies = [ 433 + "aho-corasick", 434 + "memchr", 435 + "regex-automata", 436 + "regex-syntax", 437 + ] 438 + 439 + [[package]] 440 + name = "regex-automata" 441 + version = "0.4.11" 442 + source = "registry+https://github.com/rust-lang/crates.io-index" 443 + checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" 444 + dependencies = [ 445 + "aho-corasick", 446 + "memchr", 447 + "regex-syntax", 448 + ] 449 + 450 + [[package]] 451 + name = "regex-syntax" 452 + version = "0.8.6" 453 + source = "registry+https://github.com/rust-lang/crates.io-index" 454 + checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 455 + 456 + [[package]] 457 + name = "rustc-demangle" 458 + version = "0.1.26" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 461 + 462 + [[package]] 463 + name = "rustix" 464 + version = "1.1.2" 465 + source = "registry+https://github.com/rust-lang/crates.io-index" 466 + checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 467 + dependencies = [ 468 + "bitflags", 469 + "errno", 470 + "libc", 471 + "linux-raw-sys", 472 + "windows-sys", 473 + ] 474 + 475 + [[package]] 476 + name = "rustversion" 477 + version = "1.0.22" 478 + source = "registry+https://github.com/rust-lang/crates.io-index" 479 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 480 + 481 + [[package]] 482 + name = "ryu" 483 + version = "1.0.20" 484 + source = "registry+https://github.com/rust-lang/crates.io-index" 485 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 486 + 487 + [[package]] 488 + name = "same-file" 489 + version = "1.0.6" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 492 + dependencies = [ 493 + "winapi-util", 494 + ] 495 + 496 + [[package]] 497 + name = "serde" 498 + version = "1.0.228" 499 + source = "registry+https://github.com/rust-lang/crates.io-index" 500 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 501 + dependencies = [ 502 + "serde_core", 503 + "serde_derive", 504 + ] 505 + 506 + [[package]] 507 + name = "serde-wasm-bindgen" 508 + version = "0.6.5" 509 + source = "registry+https://github.com/rust-lang/crates.io-index" 510 + checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" 511 + dependencies = [ 512 + "js-sys", 513 + "serde", 514 + "wasm-bindgen", 515 + ] 516 + 517 + [[package]] 518 + name = "serde_core" 519 + version = "1.0.228" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 522 + dependencies = [ 523 + "serde_derive", 524 + ] 525 + 526 + [[package]] 527 + name = "serde_derive" 528 + version = "1.0.228" 529 + source = "registry+https://github.com/rust-lang/crates.io-index" 530 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 531 + dependencies = [ 532 + "proc-macro2", 533 + "quote", 534 + "syn", 535 + ] 536 + 537 + [[package]] 538 + name = "serde_json" 539 + version = "1.0.145" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 542 + dependencies = [ 543 + "itoa", 544 + "memchr", 545 + "ryu", 546 + "serde", 547 + "serde_core", 548 + ] 549 + 550 + [[package]] 551 + name = "shlex" 552 + version = "1.3.0" 553 + source = "registry+https://github.com/rust-lang/crates.io-index" 554 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 555 + 556 + [[package]] 557 + name = "strsim" 558 + version = "0.11.1" 559 + source = "registry+https://github.com/rust-lang/crates.io-index" 560 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 561 + 562 + [[package]] 563 + name = "supports-color" 564 + version = "3.0.2" 565 + source = "registry+https://github.com/rust-lang/crates.io-index" 566 + checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" 567 + dependencies = [ 568 + "is_ci", 569 + ] 570 + 571 + [[package]] 572 + name = "supports-hyperlinks" 573 + version = "3.1.0" 574 + source = "registry+https://github.com/rust-lang/crates.io-index" 575 + checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" 576 + 577 + [[package]] 578 + name = "supports-unicode" 579 + version = "3.0.0" 580 + source = "registry+https://github.com/rust-lang/crates.io-index" 581 + checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 582 + 583 + [[package]] 584 + name = "syn" 585 + version = "2.0.106" 586 + source = "registry+https://github.com/rust-lang/crates.io-index" 587 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 588 + dependencies = [ 589 + "proc-macro2", 590 + "quote", 591 + "unicode-ident", 592 + ] 593 + 594 + [[package]] 595 + name = "terminal_size" 596 + version = "0.4.3" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 599 + dependencies = [ 600 + "rustix", 601 + "windows-sys", 602 + ] 603 + 604 + [[package]] 605 + name = "textwrap" 606 + version = "0.16.2" 607 + source = "registry+https://github.com/rust-lang/crates.io-index" 608 + checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 609 + dependencies = [ 610 + "unicode-linebreak", 611 + "unicode-width 0.2.1", 612 + ] 613 + 614 + [[package]] 615 + name = "thiserror" 616 + version = "2.0.17" 617 + source = "registry+https://github.com/rust-lang/crates.io-index" 618 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 619 + dependencies = [ 620 + "thiserror-impl", 621 + ] 622 + 623 + [[package]] 624 + name = "thiserror-impl" 625 + version = "2.0.17" 626 + source = "registry+https://github.com/rust-lang/crates.io-index" 627 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 628 + dependencies = [ 629 + "proc-macro2", 630 + "quote", 631 + "syn", 632 + ] 633 + 634 + [[package]] 635 + name = "tree-sitter" 636 + version = "0.22.6" 637 + source = "registry+https://github.com/rust-lang/crates.io-index" 638 + checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" 639 + dependencies = [ 640 + "cc", 641 + "regex", 642 + ] 643 + 644 + [[package]] 645 + name = "tree-sitter-mlf" 646 + version = "0.1.0" 647 + dependencies = [ 648 + "cc", 649 + "tree-sitter", 650 + ] 651 + 652 + [[package]] 653 + name = "unicode-ident" 654 + version = "1.0.19" 655 + source = "registry+https://github.com/rust-lang/crates.io-index" 656 + checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 657 + 658 + [[package]] 659 + name = "unicode-linebreak" 660 + version = "0.1.5" 661 + source = "registry+https://github.com/rust-lang/crates.io-index" 662 + checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 663 + 664 + [[package]] 665 + name = "unicode-width" 666 + version = "0.1.14" 667 + source = "registry+https://github.com/rust-lang/crates.io-index" 668 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 669 + 670 + [[package]] 671 + name = "unicode-width" 672 + version = "0.2.1" 673 + source = "registry+https://github.com/rust-lang/crates.io-index" 674 + checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 675 + 676 + [[package]] 677 + name = "utf8parse" 678 + version = "0.2.2" 679 + source = "registry+https://github.com/rust-lang/crates.io-index" 680 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 681 + 682 + [[package]] 683 + name = "walkdir" 684 + version = "2.5.0" 685 + source = "registry+https://github.com/rust-lang/crates.io-index" 686 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 687 + dependencies = [ 688 + "same-file", 689 + "winapi-util", 690 + ] 691 + 692 + [[package]] 693 + name = "wasm-bindgen" 694 + version = "0.2.104" 695 + source = "registry+https://github.com/rust-lang/crates.io-index" 696 + checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 697 + dependencies = [ 698 + "cfg-if", 699 + "once_cell", 700 + "rustversion", 701 + "wasm-bindgen-macro", 702 + "wasm-bindgen-shared", 703 + ] 704 + 705 + [[package]] 706 + name = "wasm-bindgen-backend" 707 + version = "0.2.104" 708 + source = "registry+https://github.com/rust-lang/crates.io-index" 709 + checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 710 + dependencies = [ 711 + "bumpalo", 712 + "log", 713 + "proc-macro2", 714 + "quote", 715 + "syn", 716 + "wasm-bindgen-shared", 717 + ] 718 + 719 + [[package]] 720 + name = "wasm-bindgen-futures" 721 + version = "0.4.54" 722 + source = "registry+https://github.com/rust-lang/crates.io-index" 723 + checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" 724 + dependencies = [ 725 + "cfg-if", 726 + "js-sys", 727 + "once_cell", 728 + "wasm-bindgen", 729 + "web-sys", 730 + ] 731 + 732 + [[package]] 733 + name = "wasm-bindgen-macro" 734 + version = "0.2.104" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 737 + dependencies = [ 738 + "quote", 739 + "wasm-bindgen-macro-support", 740 + ] 741 + 742 + [[package]] 743 + name = "wasm-bindgen-macro-support" 744 + version = "0.2.104" 745 + source = "registry+https://github.com/rust-lang/crates.io-index" 746 + checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 747 + dependencies = [ 748 + "proc-macro2", 749 + "quote", 750 + "syn", 751 + "wasm-bindgen-backend", 752 + "wasm-bindgen-shared", 753 + ] 754 + 755 + [[package]] 756 + name = "wasm-bindgen-shared" 757 + version = "0.2.104" 758 + source = "registry+https://github.com/rust-lang/crates.io-index" 759 + checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 760 + dependencies = [ 761 + "unicode-ident", 762 + ] 763 + 764 + [[package]] 765 + name = "wasm-bindgen-test" 766 + version = "0.3.54" 767 + source = "registry+https://github.com/rust-lang/crates.io-index" 768 + checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865" 769 + dependencies = [ 770 + "js-sys", 771 + "minicov", 772 + "wasm-bindgen", 773 + "wasm-bindgen-futures", 774 + "wasm-bindgen-test-macro", 775 + ] 776 + 777 + [[package]] 778 + name = "wasm-bindgen-test-macro" 779 + version = "0.3.54" 780 + source = "registry+https://github.com/rust-lang/crates.io-index" 781 + checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8" 782 + dependencies = [ 783 + "proc-macro2", 784 + "quote", 785 + "syn", 786 + ] 787 + 788 + [[package]] 789 + name = "web-sys" 790 + version = "0.3.81" 791 + source = "registry+https://github.com/rust-lang/crates.io-index" 792 + checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" 793 + dependencies = [ 794 + "js-sys", 795 + "wasm-bindgen", 796 + ] 797 + 798 + [[package]] 799 + name = "winapi-util" 800 + version = "0.1.11" 801 + source = "registry+https://github.com/rust-lang/crates.io-index" 802 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 803 + dependencies = [ 804 + "windows-sys", 805 + ] 806 + 807 + [[package]] 808 + name = "windows-link" 809 + version = "0.2.0" 810 + source = "registry+https://github.com/rust-lang/crates.io-index" 811 + checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 812 + 813 + [[package]] 814 + name = "windows-sys" 815 + version = "0.60.2" 816 + source = "registry+https://github.com/rust-lang/crates.io-index" 817 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 818 + dependencies = [ 819 + "windows-targets", 820 + ] 821 + 822 + [[package]] 823 + name = "windows-targets" 824 + version = "0.53.4" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" 827 + dependencies = [ 828 + "windows-link", 829 + "windows_aarch64_gnullvm", 830 + "windows_aarch64_msvc", 831 + "windows_i686_gnu", 832 + "windows_i686_gnullvm", 833 + "windows_i686_msvc", 834 + "windows_x86_64_gnu", 835 + "windows_x86_64_gnullvm", 836 + "windows_x86_64_msvc", 837 + ] 838 + 839 + [[package]] 840 + name = "windows_aarch64_gnullvm" 841 + version = "0.53.0" 842 + source = "registry+https://github.com/rust-lang/crates.io-index" 843 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 844 + 845 + [[package]] 846 + name = "windows_aarch64_msvc" 847 + version = "0.53.0" 848 + source = "registry+https://github.com/rust-lang/crates.io-index" 849 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 850 + 851 + [[package]] 852 + name = "windows_i686_gnu" 853 + version = "0.53.0" 854 + source = "registry+https://github.com/rust-lang/crates.io-index" 855 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 856 + 857 + [[package]] 858 + name = "windows_i686_gnullvm" 859 + version = "0.53.0" 860 + source = "registry+https://github.com/rust-lang/crates.io-index" 861 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 862 + 863 + [[package]] 864 + name = "windows_i686_msvc" 865 + version = "0.53.0" 866 + source = "registry+https://github.com/rust-lang/crates.io-index" 867 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 868 + 869 + [[package]] 870 + name = "windows_x86_64_gnu" 871 + version = "0.53.0" 872 + source = "registry+https://github.com/rust-lang/crates.io-index" 873 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 874 + 875 + [[package]] 876 + name = "windows_x86_64_gnullvm" 877 + version = "0.53.0" 878 + source = "registry+https://github.com/rust-lang/crates.io-index" 879 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 880 + 881 + [[package]] 882 + name = "windows_x86_64_msvc" 883 + version = "0.53.0" 884 + source = "registry+https://github.com/rust-lang/crates.io-index" 885 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+12
Cargo.toml
··· 1 + [workspace] 2 + resolver = "3" 3 + members = [ 4 + "mlf-cli", "mlf-codegen", "mlf-diagnostics", 5 + "mlf-lang", 6 + "mlf-validation", "mlf-wasm", 7 + "tree-sitter-mlf" 8 + ] 9 + 10 + default-members = [ 11 + "mlf-cli" 12 + ]
+69
README.md
··· 1 + # Matt's Lexicon Format 2 + 3 + A human-friendly DSL for ATProto Lexicons 4 + 5 + ## What it looks like 6 + 7 + ```mlf 8 + namespace com.example.post { 9 + record post { 10 + text: string constrained { 11 + maxLength: 3000, 12 + maxGraphemes: 300, 13 + }, 14 + createdAt: Datetime, 15 + reply?: replyRef, 16 + } 17 + 18 + alias replyRef = { 19 + root: com.atproto.repo.strongRef, 20 + parent: com.atproto.repo.strongRef, 21 + }; 22 + } 23 + ``` 24 + 25 + ## Getting started 26 + 27 + ### Install 28 + 29 + ```bash 30 + cargo install --path mlf-cli 31 + ``` 32 + 33 + ### Generate JSON lexicons from MLF 34 + 35 + ```bash 36 + mlf generate lexicon -i examples/**/*.mlf -o output/ 37 + ``` 38 + 39 + ### Validate MLF files 40 + 41 + ```bash 42 + mlf check examples/app.bsky.feed.post.mlf 43 + ``` 44 + 45 + ### Validate generated lexicons 46 + 47 + ```bash 48 + mlf validate output/app.bsky.feed.post.json 49 + ``` 50 + 51 + ## Project layout 52 + 53 + ``` 54 + mlf/ 55 + ├── mlf-cli/ # Command-line app 56 + ├── mlf-lang/ # Parser and lexer (no_std compatible) 57 + ├── mlf-codegen/ # JSON lexicon code generation 58 + ├── mlf-validation/ # Lexicon validation 59 + ├── mlf-diagnostics/ # Fancy error reporting 60 + ├── mlf-wasm/ # WASM bindings for browser use 61 + ├── tree-sitter-mlf/ # Tree-sitter grammar for syntax highlighting 62 + └── website/ # Docs and playground 63 + ``` 64 + 65 + ## Documentation 66 + 67 + Full documentation available at the [MLF website](https://mlf.lol) (or run `just serve` in `website/`). 68 + 69 + See [SPEC.md](SPEC.md) for the complete language specification.
+905
SPEC.md
··· 1 + # MLF (Matt's Lexicon Format) Specification 2 + 3 + ## Overview 4 + 5 + MLF is a domain-specific language (DSL) for writing ATProto Lexicons with 100% fidelity to the [AT Protocol Lexicon specification](https://atproto.com/specs/lexicon). It provides a more ergonomic, type-safe syntax for defining records, queries, procedures, and types. 6 + 7 + ## Design Goals 8 + 9 + 1. **100% ATProto Fidelity**: Every valid ATProto Lexicon can be represented in MLF 10 + 2. **Human-Readable**: Clear, concise syntax that's easy to read and write 11 + 3. **no_std Compatible**: Core parser can run in constrained environments 12 + 4. **Tooling-Friendly**: Enable validation, code generation, and formatting 13 + 14 + ## File Structure 15 + 16 + ### File Extension 17 + - `.mlf` - MLF source files 18 + 19 + ### Shebang (Optional) 20 + ```mlf 21 + #!/usr/bin/env mlf 22 + ``` 23 + 24 + The `#` character is reserved for shebangs only and is not used elsewhere in the syntax. 25 + 26 + ### File Naming Convention 27 + Files should follow the lexicon NSID: 28 + - `app.bsky.feed.post.mlf` → Lexicon NSID: `app.bsky.feed.post` 29 + - `sh.tangled.repo.issue.mlf` → Lexicon NSID: `sh.tangled.repo.issue` 30 + 31 + ## Core Concepts 32 + 33 + ### NSIDs (Namespaced Identifiers) 34 + 35 + NSIDs use dotted notation: 36 + ``` 37 + app.bsky.feed.post 38 + com.example.thing 39 + sh.tangled.repo.issue 40 + ``` 41 + 42 + - Format: `authority.name(.name)*` 43 + - Authority: Typically a reversed domain name 44 + - Segments: Lowercase letters, numbers, hyphens (no underscores) 45 + 46 + ### Lexicon Resolution 47 + 48 + References to definitions can be: 49 + 50 + 1. **Local (same file)**: Just use the name 51 + ```mlf 52 + record myRecord { 53 + field: myAlias, // References alias in same file 54 + } 55 + 56 + alias myAlias = { /* ... */ }; 57 + ``` 58 + 59 + 2. **Cross-file (different lexicon)**: Use full dotted path 60 + ```mlf 61 + 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 + } 65 + ``` 66 + 67 + **Note**: The `#` character is NOT used for references. All references use dotted notation. 68 + 69 + ## Type System 70 + 71 + ### Primitive Types 72 + 73 + ```mlf 74 + null // Null value 75 + boolean // True or false 76 + integer // 64-bit integer 77 + number // Double-precision float 78 + string // UTF-8 string 79 + bytes // Byte array 80 + ``` 81 + 82 + ### Special String Formats 83 + 84 + Defined in `prelude.mlf` and available everywhere: 85 + 86 + ```mlf 87 + Did // Decentralized Identifier (did:*) 88 + AtUri // AT-URI (at://...) 89 + AtIdentifier // Either a DID or Handle 90 + Handle // Handle identifier (domain name) 91 + Datetime // ISO 8601 datetime 92 + Uri // Generic URI 93 + Cid // Content Identifier 94 + Nsid // Namespaced Identifier 95 + Tid // Timestamp Identifier 96 + RecordKey // Record key 97 + Language // BCP 47 language code 98 + ``` 99 + 100 + ### Blob Types 101 + 102 + ```mlf 103 + blob // Generic blob 104 + ``` 105 + 106 + With constraints: 107 + ```mlf 108 + avatar: blob constrained { 109 + accept: ["image/png", "image/jpeg"], 110 + maxSize: 1000000, // bytes 111 + } 112 + ``` 113 + 114 + ### Unknown Type 115 + 116 + ```mlf 117 + unknown // Represents any value, used for forward compatibility 118 + ``` 119 + 120 + ## Definitions 121 + 122 + ### Records 123 + 124 + Records are the primary data structure, stored in repositories: 125 + 126 + ```mlf 127 + record post { 128 + text: string constrained { 129 + maxLength: 300, 130 + maxGraphemes: 300, 131 + }, 132 + createdAt: Datetime, 133 + reply?: replyRef, // Optional field 134 + } 135 + ``` 136 + 137 + ### Aliases 138 + 139 + Type aliases define reusable object shapes: 140 + 141 + ```mlf 142 + alias replyRef = { 143 + root: AtUri, 144 + parent: AtUri, 145 + }; 146 + ``` 147 + 148 + If used in multiple places, they will be hoisted to a def. If only used in a single place, they will be inlined. 149 + 150 + ### Tokens 151 + 152 + Tokens are named constants used in enums and unions: 153 + 154 + ```mlf 155 + /// Open state 156 + token open; 157 + 158 + /// Closed state 159 + token closed; 160 + 161 + record issue { 162 + state: string constrained { 163 + knownValues: [ 164 + open, // References token defined above 165 + closed, 166 + ], 167 + default: "open", 168 + }, 169 + } 170 + ``` 171 + 172 + Tokens must have doc comments describing their purpose. 173 + 174 + ### Queries 175 + 176 + Queries are read-only HTTP endpoints (GET): 177 + 178 + ```mlf 179 + /// Get a user profile 180 + query getProfile( 181 + /// The actor's DID or handle 182 + actor: AtIdentifier, 183 + /// Optional viewer context 184 + viewer?: Did, 185 + ): profileView | error { 186 + /// Profile not found 187 + ProfileNotFound, 188 + /// Invalid request parameters 189 + BadRequest, 190 + }; 191 + ``` 192 + 193 + ### Procedures 194 + 195 + Procedures are write operations (POST): 196 + 197 + ```mlf 198 + /// Create a new post 199 + procedure createPost( 200 + text: string, 201 + createdAt: Datetime, 202 + ): { 203 + uri: AtUri, 204 + cid: Cid, 205 + } | error { 206 + /// Text exceeds maximum length 207 + TextTooLong, 208 + }; 209 + ``` 210 + 211 + ### Subscriptions 212 + 213 + Subscriptions are WebSocket-based event streams that emit messages over time. They are used for real-time updates and event notifications. 214 + 215 + ```mlf 216 + /// Subscribe to repository events 217 + subscription subscribeRepos( 218 + /// Optional cursor for resuming from a specific point 219 + cursor?: integer, 220 + ): commit | identity | handle | migrate | tombstone | info; 221 + ``` 222 + 223 + **Message definitions** for subscriptions are defined as aliases or records: 224 + 225 + ```mlf 226 + /// 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, 239 + }; 240 + 241 + /// Info message 242 + alias info = { 243 + name: string, 244 + message?: string, 245 + }; 246 + ``` 247 + 248 + **Subscription features:** 249 + 250 + - Parameters: Like queries, subscriptions can have parameters 251 + - Return type: A union of message types that can be emitted 252 + - Each message type must be defined as an alias or record 253 + - Message types can be local or imported from other lexicons 254 + - Subscriptions are long-lived WebSocket connections 255 + - No error block (errors are handled at the WebSocket protocol level) 256 + 257 + **Example: Chat message subscription** 258 + 259 + ```mlf 260 + /// Subscribe to chat messages for a stream 261 + subscription subscribeChat( 262 + /// The DID of the streamer 263 + streamer: Did, 264 + /// Optional cursor to resume from 265 + cursor?: string, 266 + ): message | delete | join | leave; 267 + 268 + /// Chat message payload 269 + alias message = { 270 + id: string, 271 + text: string, 272 + author: Did, 273 + createdAt: Datetime, 274 + }; 275 + 276 + /// Delete event payload 277 + alias delete = { 278 + id: string, 279 + }; 280 + 281 + /// Join event payload 282 + alias join = { 283 + user: Did, 284 + }; 285 + 286 + /// Leave event payload 287 + alias leave = { 288 + user: Did, 289 + }; 290 + ``` 291 + 292 + ### Return Types 293 + 294 + Queries and procedures can return: 295 + 296 + 1. **Simple success**: `(): returnType` 297 + 2. **Success with errors**: `(): successType | error { ErrorName, ... }` 298 + - Each error must have a doc comment describing it 299 + 3. **Unknown/empty**: `(): unknown` 300 + 301 + ## Type Modifiers 302 + 303 + ### Optional Fields 304 + 305 + ```mlf 306 + record example { 307 + required: string, 308 + optional?: string, 309 + } 310 + ``` 311 + 312 + ### Arrays 313 + 314 + ```mlf 315 + record example { 316 + tags: string[], 317 + items: string[] constrained { 318 + minLength: 1, 319 + maxLength: 10, 320 + }, 321 + } 322 + ``` 323 + 324 + ### Unions 325 + 326 + Use the pipe operator `|`: 327 + 328 + ```mlf 329 + record example { 330 + // Closed union (only these types) 331 + content: text | image | video, 332 + 333 + // Union of tokens 334 + state: open | closed | pending, 335 + } 336 + ``` 337 + 338 + Open unions (allowing unknown types) use `_`: 339 + 340 + ```mlf 341 + record example { 342 + // Open union (can include unknown types) 343 + content: text | image | _, 344 + } 345 + ``` 346 + 347 + ### References 348 + 349 + Reference local or external definitions: 350 + 351 + ```mlf 352 + // Local reference (same file) 353 + record post { 354 + author: author, // References 'alias author' in same file 355 + } 356 + 357 + // Cross-file reference 358 + record post { 359 + profile: app.bsky.actor.profile, // References app/bsky/actor/profile.mlf 360 + } 361 + ``` 362 + 363 + ## Constraints 364 + 365 + Constraints refine types by adding additional restrictions. A key principle is that constraints can only make types **more restrictive**, never less restrictive. This ensures type safety and proper substitutability. 366 + 367 + ### Constraint Refinement Rules 368 + 369 + When applying constraints, each constraint must be **at least as restrictive** as any parent constraint: 370 + 371 + ```mlf 372 + // Valid: More restrictive constraints 373 + alias shortString = string constrained { 374 + maxLength: 100, 375 + }; 376 + 377 + record post { 378 + // Can further constrain to 50 (more restrictive than 100) 379 + title: shortString constrained { 380 + maxLength: 50, // ✓ Valid: 50 ≤ 100 381 + }, 382 + } 383 + 384 + // Invalid: Less restrictive constraints 385 + record invalid { 386 + // ERROR: Cannot expand to 200 (less restrictive than 100) 387 + content: shortString constrained { 388 + maxLength: 200, // ✗ Invalid: 200 > 100 389 + }, 390 + } 391 + ``` 392 + 393 + **Refinement rules by constraint type:** 394 + 395 + - **Numeric bounds**: `minimum` can only increase, `maximum` can only decrease 396 + - **Length bounds**: `minLength`/`minGraphemes` can only increase, `maxLength`/`maxGraphemes` can only decrease 397 + - **Enums**: Can only restrict to a subset of values 398 + - **Known values**: Can add new values (extensible) but cannot remove specified ones 399 + - **Format**: Cannot change once specified 400 + - **Defaults**: Can be specified if not already set 401 + 402 + ### String Constraints 403 + 404 + ```mlf 405 + 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 417 + } 418 + ``` 419 + 420 + ### Integer Constraints 421 + 422 + ```mlf 423 + field: integer constrained { 424 + minimum: 0, 425 + maximum: 100, 426 + enum: [1, 2, 3], 427 + default: 1, 428 + } 429 + ``` 430 + 431 + ### Array Constraints 432 + 433 + ```mlf 434 + field: string[] constrained { 435 + minLength: 1, 436 + maxLength: 10, 437 + } 438 + ``` 439 + 440 + ### Blob Constraints 441 + 442 + ```mlf 443 + field: blob constrained { 444 + accept: ["image/png", "image/jpeg"], // MIME types 445 + maxSize: 1000000, // Bytes 446 + } 447 + ``` 448 + 449 + ### Boolean Constraints 450 + 451 + ```mlf 452 + field: boolean constrained { 453 + default: false, 454 + } 455 + ``` 456 + 457 + ## Comments 458 + 459 + ### Documentation Comments 460 + 461 + Use `///` for documentation (appears in generated docs/code): 462 + 463 + ```mlf 464 + /// A user profile record 465 + record profile { 466 + /// The user's display name 467 + displayName?: string, 468 + } 469 + ``` 470 + 471 + ### Regular Comments 472 + 473 + Regular comments (`//`) are ignored when processing and will have no impact on any output. 474 + 475 + ## Annotations 476 + 477 + Annotations use the `@` symbol and are metadata markers for external tooling. MLF itself assigns no semantic meaning to annotations - they are purely for tools, linters, code generators, and other processors to interpret. 478 + 479 + ### Annotation Syntax 480 + 481 + Three forms of annotations are supported: 482 + 483 + **1. Simple annotation:** 484 + ```mlf 485 + @deprecated 486 + record oldRecord { 487 + field: string, 488 + } 489 + ``` 490 + 491 + **2. Positional arguments:** 492 + ```mlf 493 + @since(1, 2, 0) 494 + @doc("https://example.com/docs") 495 + record example { 496 + field: string, 497 + } 498 + ``` 499 + 500 + Arguments can be: 501 + - Strings: `"value"` 502 + - Numbers: `42`, `3.14` 503 + - Booleans: `true`, `false` 504 + 505 + **3. Named arguments:** 506 + ```mlf 507 + @validate(min: 0, max: 100, strict: true) 508 + @codegen(language: "rust", derive: "Debug, Clone") 509 + record example { 510 + field: integer, 511 + } 512 + ``` 513 + 514 + ### Annotation Placement 515 + 516 + Annotations can be placed on: 517 + - Records 518 + - Aliases 519 + - Tokens 520 + - Queries 521 + - Procedures 522 + - Subscriptions 523 + - Fields within records/aliases 524 + 525 + ```mlf 526 + /// A user profile 527 + @table(name: "profiles", indexes: "did,handle") 528 + record profile { 529 + /// User's DID 530 + @indexed 531 + did: Did, 532 + 533 + /// Display name 534 + @sensitive(pii: true) 535 + displayName?: string, 536 + } 537 + ``` 538 + 539 + ### Common Annotation Examples 540 + 541 + ```mlf 542 + // Deprecation 543 + @deprecated 544 + @deprecated(since: "2.0.0", replacement: "newRecord") 545 + record oldRecord { /* ... */ } 546 + 547 + // Code generation hints 548 + @derive("Debug, Clone, Serialize") 549 + @table(name: "users") 550 + record user { /* ... */ } 551 + 552 + // Validation 553 + @validate(custom: "validateEmail") 554 + @range(min: 0, max: 100) 555 + field: integer 556 + 557 + // Documentation 558 + @example("did:plc:abc123") 559 + @see("https://atproto.com/specs/did") 560 + field: Did 561 + 562 + // Versioning 563 + @since(1, 0, 0) 564 + @unstable 565 + record experimentalFeature { /* ... */ } 566 + ``` 567 + 568 + **Note:** The interpretation of annotations is entirely up to the tooling consuming the MLF. Different tools may support different annotation sets. 569 + 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 + ## Use Statements 593 + 594 + Import definitions from other lexicons: 595 + 596 + ```mlf 597 + // Named imports 598 + use app.bsky.actor.{profile, profileView}; 599 + use sh.tangled.repo.issue.{issue, open, closed}; 600 + 601 + // Alias entire namespace 602 + use app.bsky.actor as Actor; 603 + 604 + // Wildcard import 605 + use app.bsky.feed.*; 606 + 607 + // Mixed 608 + use sh.tangled.repo.issue.{issue as IssueRecord, open, closed}; 609 + ``` 610 + 611 + After importing, use the short name: 612 + 613 + ```mlf 614 + use app.bsky.actor.profile; 615 + 616 + record myThing { 617 + author: profile, // Instead of app.bsky.actor.profile 618 + } 619 + ``` 620 + 621 + ## Lexicon Discovery & Resolution 622 + 623 + ### File Discovery 624 + 625 + Tools discover lexicons explicit paths: Single file, list of files, or glob pattern 626 + 627 + ```bash 628 + mlf validate app.bsky.feed.post.mlf 629 + mlf validate *.mlf 630 + mlf validate "**/*.mlf" 631 + ``` 632 + 633 + ### Resolution Order 634 + 635 + When resolving cross-file references: 636 + 637 + 1. Current file (local definitions) 638 + 2. Explicitly imported lexicons (via `use`) 639 + 3. Configured lexicon paths 640 + 4. (Future) Remote fetch via ATProto 641 + 642 + ### File Path Convention 643 + 644 + Lexicons follow a directory structure matching their NSID: 645 + 646 + ``` 647 + lexicons/ 648 + app/ 649 + bsky/ 650 + actor/ 651 + profile.mlf → app.bsky.actor.profile 652 + feed/ 653 + post.mlf → app.bsky.feed.post 654 + com/ 655 + example/ 656 + thing.mlf → com.example.thing 657 + ``` 658 + 659 + Or flat with dots in filename: 660 + ``` 661 + lexicons/ 662 + app.bsky.actor.profile.mlf 663 + app.bsky.feed.post.mlf 664 + com.example.thing.mlf 665 + ``` 666 + 667 + ## CLI Commands 668 + 669 + ```bash 670 + # Generation 671 + mlf generate code --input "**/*.mlf" --plugin rust src/* 672 + mlf generate lexicon --input "**/*.mlf" lexicons/* 673 + mlf generate example --input "**/*.mlf" --count 5 examples/* 674 + 675 + # Validate lexicons 676 + mlf validate <files|globs> 677 + mlf validate "**/*.mlf" 678 + 679 + # Format lexicons 680 + mlf fmt <files|globs> 681 + 682 + # Validate a record against a lexicon 683 + mlf check --input app.bsky.feed.post.mlf ./record.json 684 + ``` 685 + 686 + ## Examples 687 + 688 + ### Complete Lexicon Example 689 + 690 + ```mlf 691 + #!/usr/bin/env mlf 692 + 693 + use app.bsky.actor.profile; 694 + 695 + /// Open issue state 696 + token open; 697 + 698 + /// Closed issue state 699 + token closed; 700 + 701 + /// An issue in a repository 702 + record issue { 703 + /// The repository this issue belongs to 704 + repo: AtUri, 705 + /// Issue title 706 + title: string constrained { 707 + minGraphemes: 1, 708 + maxGraphemes: 200, 709 + }, 710 + /// Issue body (markdown) 711 + body?: string constrained { 712 + maxGraphemes: 10000, 713 + }, 714 + /// Issue state 715 + state: string constrained { 716 + knownValues: [ 717 + open, 718 + closed, 719 + ], 720 + default: "open", 721 + }, 722 + /// Creation timestamp 723 + createdAt: Datetime, 724 + } 725 + 726 + /// A comment on an issue 727 + record comment { 728 + /// The issue this comment belongs to 729 + issue: AtUri, 730 + /// Comment body (markdown) 731 + body: string constrained { 732 + minGraphemes: 1, 733 + maxGraphemes: 10000, 734 + }, 735 + /// Creation timestamp 736 + createdAt: Datetime, 737 + /// Optional reply target 738 + replyTo?: AtUri, 739 + } 740 + 741 + /// Get an issue by URI 742 + query getIssue( 743 + /// Issue AT-URI 744 + uri: AtUri, 745 + ): issue | error { 746 + /// Issue not found 747 + NotFound, 748 + }; 749 + 750 + /// Create a new issue 751 + procedure createIssue( 752 + repo: AtUri, 753 + title: string, 754 + body?: string, 755 + ): { 756 + uri: AtUri, 757 + cid: Cid, 758 + } | error { 759 + /// Repository not found 760 + RepoNotFound, 761 + /// Title too long 762 + TitleTooLong, 763 + }; 764 + ``` 765 + 766 + ## ATProto Mapping 767 + 768 + ### MLF → JSON Lexicon 769 + 770 + MLF compiles to standard ATProto JSON Lexicons: 771 + 772 + **MLF:** 773 + ```mlf 774 + record post { 775 + text: string constrained { 776 + maxLength: 300, 777 + }, 778 + createdAt: Datetime, 779 + } 780 + ``` 781 + 782 + **JSON:** 783 + ```json 784 + { 785 + "lexicon": 1, 786 + "id": "app.bsky.feed.post", 787 + "defs": { 788 + "main": { 789 + "type": "record", 790 + "key": "tid", 791 + "record": { 792 + "type": "object", 793 + "required": ["text", "createdAt"], 794 + "properties": { 795 + "text": { 796 + "type": "string", 797 + "maxLength": 300 798 + }, 799 + "createdAt": { 800 + "type": "string", 801 + "format": "datetime" 802 + } 803 + } 804 + } 805 + } 806 + } 807 + } 808 + ``` 809 + 810 + ### Subscription Mapping 811 + 812 + **MLF:** 813 + ```mlf 814 + subscription subscribeRepos( 815 + cursor?: integer, 816 + ): commit | identity; 817 + ``` 818 + 819 + **JSON:** 820 + ```json 821 + { 822 + "lexicon": 1, 823 + "id": "com.atproto.sync.subscribeRepos", 824 + "defs": { 825 + "main": { 826 + "type": "subscription", 827 + "parameters": { 828 + "type": "params", 829 + "properties": { 830 + "cursor": { 831 + "type": "integer" 832 + } 833 + } 834 + }, 835 + "message": { 836 + "schema": { 837 + "type": "union", 838 + "refs": ["#commit", "#identity"] 839 + } 840 + } 841 + }, 842 + "commit": { 843 + "type": "object", 844 + "required": ["seq", "repo", "commit"], 845 + "properties": { 846 + "seq": { "type": "integer" }, 847 + "repo": { "type": "string", "format": "did" }, 848 + "commit": { "type": "string", "format": "cid" } 849 + } 850 + } 851 + } 852 + } 853 + ``` 854 + 855 + ## Future Considerations 856 + 857 + ### Potential Extensions 858 + 859 + - **Version constraints**: Specify compatible lexicon versions in lexicon headers 860 + - **Custom validation**: Pluggable validators beyond built-in constraints 861 + - **Documentation generation**: Automatic API docs from MLF with annotation support 862 + - **Standard annotation registry**: Common annotations like `@deprecated`, `@since`, `@internal` 863 + - **Import resolution**: Remote lexicon fetching and caching 864 + - **Type inference**: Automatic type inference for constrained types 865 + 866 + ### Versioning 867 + 868 + Lexicons are versioned at the NSID level. MLF files should include version metadata in comments or future version declarations. 869 + 870 + ## Appendix 871 + 872 + ### Reserved Keywords 873 + 874 + ``` 875 + alias, as, blob, boolean, bytes, constrained, error, integer, 876 + namespace, null, number, procedure, query, record, string, 877 + subscription, token, unknown, use 878 + ``` 879 + 880 + ### Raw Identifiers 881 + 882 + To use a reserved keyword as an identifier, wrap it in backticks: 883 + 884 + ```mlf 885 + alias `record` = { 886 + `record`: com.atproto.repo.strongRef, 887 + `error`: string, 888 + }; 889 + ``` 890 + 891 + This allows field names or type names to match reserved keywords when necessary for compatibility with existing schemas. 892 + 893 + ### Constraint Keywords 894 + 895 + ``` 896 + accept, default, enum, format, knownValues, maxGraphemes, 897 + maxLength, maxSize, maximum, minGraphemes, minLength, minimum 898 + ``` 899 + 900 + ### Format Values 901 + 902 + ``` 903 + at-identifier, at-uri, cid, datetime, did, handle, language, 904 + nsid, record-key, tid, uri 905 + ```
+20
mlf-cli/Cargo.toml
··· 1 + [package] 2 + name = "mlf-cli" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [[bin]] 7 + name = "mlf" 8 + path = "src/main.rs" 9 + 10 + [dependencies] 11 + mlf-lang = { path = "../mlf-lang" } 12 + mlf-validation = { path = "../mlf-validation" } 13 + mlf-codegen = { path = "../mlf-codegen" } 14 + mlf-diagnostics = { path = "../mlf-diagnostics" } 15 + clap = { version = "4.5.48", features = ["derive"] } 16 + miette = { version = "7", features = ["fancy"] } 17 + thiserror = "2" 18 + serde = { version = "1", features = ["derive"] } 19 + serde_json = "1" 20 + glob = "0.3"
+192
mlf-cli/src/check.rs
··· 1 + use miette::Diagnostic; 2 + use mlf_diagnostics::{ParseDiagnostic, ValidationDiagnostic}; 3 + use std::path::PathBuf; 4 + use thiserror::Error; 5 + 6 + #[derive(Error, Debug, Diagnostic)] 7 + pub enum CheckError { 8 + #[error("Failed to read file: {path}")] 9 + #[diagnostic(code(mlf::check::read_file))] 10 + ReadFile { 11 + path: String, 12 + #[source] 13 + source: std::io::Error, 14 + }, 15 + 16 + #[error("Failed to parse lexicon: {path}")] 17 + #[diagnostic(code(mlf::check::parse_lexicon))] 18 + ParseLexicon { 19 + path: String, 20 + #[help] 21 + help: Option<String>, 22 + }, 23 + 24 + #[error("Failed to parse JSON record")] 25 + #[diagnostic(code(mlf::check::parse_json))] 26 + ParseJson { 27 + #[source] 28 + source: serde_json::Error, 29 + }, 30 + 31 + #[error("Validation errors in lexicon")] 32 + #[diagnostic(code(mlf::check::validation_errors))] 33 + ValidationErrors { 34 + #[help] 35 + help: Option<String>, 36 + }, 37 + 38 + #[error("Failed to expand glob pattern")] 39 + #[diagnostic(code(mlf::check::glob_error))] 40 + GlobError { 41 + #[source] 42 + source: glob::GlobError, 43 + }, 44 + 45 + #[error("Invalid glob pattern: {pattern}")] 46 + #[diagnostic(code(mlf::check::invalid_glob))] 47 + InvalidGlob { 48 + pattern: String, 49 + #[source] 50 + source: glob::PatternError, 51 + }, 52 + 53 + #[error("Record validation failed")] 54 + #[diagnostic(code(mlf::check::record_validation))] 55 + RecordValidation { 56 + errors: Vec<mlf_validation::ValidationError>, 57 + }, 58 + } 59 + 60 + pub fn run(input_patterns: Vec<String>) -> Result<(), CheckError> { 61 + let mut file_paths = Vec::new(); 62 + 63 + for pattern in input_patterns { 64 + if pattern.contains('*') || pattern.contains('?') { 65 + for entry in glob::glob(&pattern).map_err(|source| CheckError::InvalidGlob { 66 + pattern: pattern.clone(), 67 + source, 68 + })? { 69 + let path = entry.map_err(|source| CheckError::GlobError { source })?; 70 + file_paths.push(path); 71 + } 72 + } else { 73 + file_paths.push(PathBuf::from(pattern)); 74 + } 75 + } 76 + 77 + let mut workspace = mlf_lang::Workspace::with_prelude().map_err(|e| { 78 + CheckError::ValidationErrors { 79 + help: Some(format!("Failed to load prelude: {:?}", e)), 80 + } 81 + })?; 82 + 83 + let mut source_files = Vec::new(); 84 + let mut had_parse_errors = false; 85 + 86 + for file_path in &file_paths { 87 + let source = std::fs::read_to_string(file_path).map_err(|source| { 88 + CheckError::ReadFile { 89 + path: file_path.display().to_string(), 90 + source, 91 + } 92 + })?; 93 + 94 + let filename = file_path.display().to_string(); 95 + 96 + let lexicon = match mlf_lang::parse_lexicon(&source) { 97 + Ok(lex) => lex, 98 + Err(e) => { 99 + let diagnostic = ParseDiagnostic::new(filename.clone(), source.clone(), e); 100 + eprintln!("{:?}", miette::Report::new(diagnostic)); 101 + had_parse_errors = true; 102 + continue; 103 + } 104 + }; 105 + 106 + let namespace = file_path 107 + .file_stem() 108 + .and_then(|s| s.to_str()) 109 + .unwrap_or("unknown") 110 + .to_string(); 111 + 112 + if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { 113 + let diagnostic = ValidationDiagnostic::new(filename.clone(), source.clone(), e); 114 + eprintln!("{:?}", miette::Report::new(diagnostic)); 115 + had_parse_errors = true; 116 + continue; 117 + } 118 + 119 + source_files.push((filename.clone(), source)); 120 + println!("✓ {}: Parsed successfully", file_path.display()); 121 + } 122 + 123 + if had_parse_errors { 124 + return Err(CheckError::ValidationErrors { 125 + help: Some("Some lexicons had parse errors".to_string()), 126 + }); 127 + } 128 + 129 + if let Err(e) = workspace.resolve() { 130 + // Show all errors from the first source file 131 + if let Some((filename, source)) = source_files.first() { 132 + let diagnostic = ValidationDiagnostic::new(filename.clone(), source.clone(), e); 133 + eprintln!("{:?}", miette::Report::new(diagnostic)); 134 + } 135 + return Err(CheckError::ValidationErrors { 136 + help: Some("Workspace validation failed".to_string()), 137 + }); 138 + } 139 + 140 + println!("\n✓ All lexicons are valid"); 141 + Ok(()) 142 + } 143 + 144 + pub fn validate(lexicon_path: PathBuf, record_path: PathBuf) -> Result<(), CheckError> { 145 + let lexicon_source = std::fs::read_to_string(&lexicon_path).map_err(|source| { 146 + CheckError::ReadFile { 147 + path: lexicon_path.display().to_string(), 148 + source, 149 + } 150 + })?; 151 + 152 + let record_source = std::fs::read_to_string(&record_path).map_err(|source| { 153 + CheckError::ReadFile { 154 + path: record_path.display().to_string(), 155 + source, 156 + } 157 + })?; 158 + 159 + let lexicon = mlf_lang::parse_lexicon(&lexicon_source).map_err(|e| { 160 + let diagnostic = ParseDiagnostic::new( 161 + lexicon_path.display().to_string(), 162 + lexicon_source.clone(), 163 + e, 164 + ); 165 + eprintln!("{:?}", miette::Report::new(diagnostic)); 166 + CheckError::ParseLexicon { 167 + path: lexicon_path.display().to_string(), 168 + help: Some("Failed to parse lexicon".to_string()), 169 + } 170 + })?; 171 + 172 + let record: serde_json::Value = serde_json::from_str(&record_source) 173 + .map_err(|source| CheckError::ParseJson { source })?; 174 + 175 + println!("✓ Lexicon parsed successfully"); 176 + println!("✓ JSON record parsed successfully"); 177 + 178 + let validator = mlf_validation::RecordValidator::new(&lexicon); 179 + match validator.validate_record(&record) { 180 + Ok(()) => { 181 + println!("✓ Record is valid according to the lexicon schema"); 182 + Ok(()) 183 + } 184 + Err(errors) => { 185 + eprintln!("✗ Record validation failed with {} error(s):", errors.len()); 186 + for error in &errors { 187 + eprintln!(" • {}", error); 188 + } 189 + Err(CheckError::RecordValidation { errors }) 190 + } 191 + } 192 + }
+1
mlf-cli/src/generate.rs
··· 1 + pub mod lexicon;
+151
mlf-cli/src/generate/lexicon.rs
··· 1 + use miette::Diagnostic; 2 + use std::path::{Path, PathBuf}; 3 + use thiserror::Error; 4 + 5 + #[derive(Error, Debug, Diagnostic)] 6 + pub enum GenerateError { 7 + #[error("Failed to read file: {path}")] 8 + #[diagnostic(code(mlf::generate::read_file))] 9 + ReadFile { 10 + path: String, 11 + #[source] 12 + source: std::io::Error, 13 + }, 14 + 15 + #[error("Failed to parse lexicon: {path}")] 16 + #[diagnostic(code(mlf::generate::parse_lexicon))] 17 + ParseLexicon { 18 + path: String, 19 + #[help] 20 + help: Option<String>, 21 + }, 22 + 23 + #[error("Failed to write output: {path}")] 24 + #[diagnostic(code(mlf::generate::write_output))] 25 + WriteOutput { 26 + path: String, 27 + #[source] 28 + source: std::io::Error, 29 + }, 30 + 31 + #[error("Failed to expand glob pattern")] 32 + #[diagnostic(code(mlf::generate::glob_error))] 33 + GlobError { 34 + #[source] 35 + source: glob::GlobError, 36 + }, 37 + 38 + #[error("Invalid glob pattern: {pattern}")] 39 + #[diagnostic(code(mlf::generate::invalid_glob))] 40 + InvalidGlob { 41 + pattern: String, 42 + #[source] 43 + source: glob::PatternError, 44 + }, 45 + } 46 + 47 + pub fn run(input_patterns: Vec<String>, output_dir: PathBuf, flat: bool) -> Result<(), GenerateError> { 48 + let mut file_paths = Vec::new(); 49 + 50 + for pattern in input_patterns { 51 + if pattern.contains('*') || pattern.contains('?') { 52 + for entry in glob::glob(&pattern).map_err(|source| GenerateError::InvalidGlob { 53 + pattern: pattern.clone(), 54 + source, 55 + })? { 56 + let path = entry.map_err(|source| GenerateError::GlobError { source })?; 57 + file_paths.push(path); 58 + } 59 + } else { 60 + file_paths.push(PathBuf::from(pattern)); 61 + } 62 + } 63 + 64 + std::fs::create_dir_all(&output_dir).map_err(|source| GenerateError::WriteOutput { 65 + path: output_dir.display().to_string(), 66 + source, 67 + })?; 68 + 69 + let mut errors = Vec::new(); 70 + let mut success_count = 0; 71 + 72 + for file_path in file_paths { 73 + let source = match std::fs::read_to_string(&file_path) { 74 + Ok(s) => s, 75 + Err(source) => { 76 + errors.push((file_path.display().to_string(), format!("Failed to read file: {}", source))); 77 + continue; 78 + } 79 + }; 80 + 81 + let lexicon = match mlf_lang::parse_lexicon(&source) { 82 + Ok(lex) => lex, 83 + Err(e) => { 84 + errors.push((file_path.display().to_string(), format!("{:?}", e))); 85 + continue; 86 + } 87 + }; 88 + 89 + let namespace = extract_namespace(&file_path, &lexicon); 90 + 91 + let json_lexicon = mlf_codegen::generate_lexicon(&namespace, &lexicon); 92 + 93 + let output_path = if flat { 94 + output_dir.join(format!("{}.json", namespace)) 95 + } else { 96 + let mut path = output_dir.clone(); 97 + for segment in namespace.split('.') { 98 + path.push(segment); 99 + } 100 + if let Err(source) = std::fs::create_dir_all(&path.parent().unwrap()) { 101 + errors.push((file_path.display().to_string(), format!("Failed to create directory: {}", source))); 102 + continue; 103 + } 104 + path.set_extension("json"); 105 + path 106 + }; 107 + 108 + let json_str = serde_json::to_string_pretty(&json_lexicon).unwrap(); 109 + if let Err(source) = std::fs::write(&output_path, json_str) { 110 + errors.push((output_path.display().to_string(), format!("Failed to write file: {}", source))); 111 + continue; 112 + } 113 + 114 + println!("Generated: {}", output_path.display()); 115 + success_count += 1; 116 + } 117 + 118 + if !errors.is_empty() { 119 + eprintln!("\n{} file(s) generated successfully, {} error(s) encountered:\n", success_count, errors.len()); 120 + for (path, error) in &errors { 121 + eprintln!(" {} - {}", path, error); 122 + } 123 + eprintln!(); 124 + return Err(GenerateError::ParseLexicon { 125 + path: "multiple files".to_string(), 126 + help: Some(format!("{} errors total", errors.len())), 127 + }); 128 + } 129 + 130 + println!("\nSuccessfully generated {} file(s)", success_count); 131 + Ok(()) 132 + } 133 + 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 + 146 + file_path 147 + .file_stem() 148 + .and_then(|s| s.to_str()) 149 + .unwrap_or("unknown") 150 + .to_string() 151 + }
+73
mlf-cli/src/main.rs
··· 1 + use clap::{Parser, Subcommand}; 2 + use miette::IntoDiagnostic; 3 + use std::path::PathBuf; 4 + use std::process; 5 + 6 + mod check; 7 + mod generate; 8 + 9 + #[derive(Parser)] 10 + #[command(name = "mlf")] 11 + #[command(about = "MLF (Matt's Lexicon Format) CLI tool", long_about = None)] 12 + struct Cli { 13 + #[command(subcommand)] 14 + command: Commands, 15 + } 16 + 17 + #[derive(Subcommand)] 18 + enum Commands { 19 + Check { 20 + #[arg(help = "MLF lexicon file(s) to validate (glob patterns supported)")] 21 + input: Vec<String>, 22 + }, 23 + 24 + Validate { 25 + #[arg(help = "MLF lexicon file")] 26 + lexicon: PathBuf, 27 + 28 + #[arg(help = "JSON record file to validate against the lexicon")] 29 + record: PathBuf, 30 + }, 31 + 32 + Generate { 33 + #[command(subcommand)] 34 + command: GenerateCommands, 35 + }, 36 + } 37 + 38 + #[derive(Subcommand)] 39 + enum GenerateCommands { 40 + Lexicon { 41 + #[arg(short, long, help = "Input MLF files (glob patterns supported)")] 42 + input: Vec<String>, 43 + 44 + #[arg(short, long, help = "Output directory")] 45 + output: PathBuf, 46 + 47 + #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.json)")] 48 + flat: bool, 49 + }, 50 + } 51 + 52 + fn main() { 53 + let cli = Cli::parse(); 54 + 55 + let result: Result<(), miette::Report> = match cli.command { 56 + Commands::Check { input } => { 57 + check::run(input).into_diagnostic() 58 + } 59 + Commands::Validate { lexicon, record } => { 60 + check::validate(lexicon, record).into_diagnostic() 61 + } 62 + Commands::Generate { command } => match command { 63 + GenerateCommands::Lexicon { input, output, flat } => { 64 + generate::lexicon::run(input, output, flat).into_diagnostic() 65 + } 66 + }, 67 + }; 68 + 69 + if let Err(e) = result { 70 + eprintln!("{:?}", e); 71 + process::exit(1); 72 + } 73 + }
+8
mlf-codegen/Cargo.toml
··· 1 + [package] 2 + name = "mlf-codegen" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + mlf-lang = { path = "../mlf-lang" } 8 + serde_json = "1"
+461
mlf-codegen/src/lib.rs
··· 1 + use mlf_lang::ast::*; 2 + use serde_json::{json, Map, Value}; 3 + use std::collections::HashMap; 4 + 5 + pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon) -> Value { 6 + let usage_counts = analyze_type_usage(lexicon); 7 + 8 + let mut defs = Map::new(); 9 + let mut main_def: Option<Value> = None; 10 + 11 + for item in &lexicon.items { 12 + match item { 13 + Item::Record(record) => { 14 + let record_json = generate_record_json(record, &usage_counts); 15 + main_def = Some(record_json); 16 + } 17 + Item::Query(query) => { 18 + let query_json = generate_query_json(query, &usage_counts); 19 + main_def = Some(query_json); 20 + } 21 + Item::Procedure(procedure) => { 22 + let procedure_json = generate_procedure_json(procedure, &usage_counts); 23 + main_def = Some(procedure_json); 24 + } 25 + Item::Subscription(subscription) => { 26 + let subscription_json = generate_subscription_json(subscription, &usage_counts); 27 + main_def = Some(subscription_json); 28 + } 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 + } 34 + } 35 + Item::Token(token) => { 36 + let token_json = json!({ 37 + "type": "token", 38 + "description": extract_docs(&token.docs) 39 + }); 40 + defs.insert(token.name.name.clone(), token_json); 41 + } 42 + _ => {} 43 + } 44 + } 45 + 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 + }) 55 + } 56 + 57 + fn analyze_type_usage(lexicon: &Lexicon) -> HashMap<String, usize> { 58 + let mut usage_counts = HashMap::new(); 59 + 60 + for item in &lexicon.items { 61 + match item { 62 + Item::Record(record) => { 63 + for field in &record.fields { 64 + count_type_references(&field.ty, &mut usage_counts); 65 + } 66 + } 67 + Item::Query(query) => { 68 + for param in &query.params { 69 + count_type_references(&param.ty, &mut usage_counts); 70 + } 71 + match &query.returns { 72 + ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts), 73 + ReturnType::TypeWithErrors { success, .. } => { 74 + count_type_references(success, &mut usage_counts) 75 + } 76 + } 77 + } 78 + Item::Procedure(procedure) => { 79 + for param in &procedure.params { 80 + count_type_references(&param.ty, &mut usage_counts); 81 + } 82 + match &procedure.returns { 83 + ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts), 84 + ReturnType::TypeWithErrors { success, .. } => { 85 + count_type_references(success, &mut usage_counts) 86 + } 87 + } 88 + } 89 + Item::Subscription(subscription) => { 90 + for param in &subscription.params { 91 + count_type_references(&param.ty, &mut usage_counts); 92 + } 93 + count_type_references(&subscription.messages, &mut usage_counts); 94 + } 95 + Item::Alias(alias) => { 96 + count_type_references(&alias.ty, &mut usage_counts); 97 + } 98 + _ => {} 99 + } 100 + } 101 + 102 + usage_counts 103 + } 104 + 105 + fn count_type_references(ty: &Type, counts: &mut HashMap<String, usize>) { 106 + match ty { 107 + Type::Reference { path, .. } => { 108 + if path.segments.len() == 1 { 109 + let name = &path.segments[0].name; 110 + *counts.entry(name.clone()).or_insert(0) += 1; 111 + } 112 + } 113 + Type::Array { inner, .. } => count_type_references(inner, counts), 114 + Type::Union { types, .. } => { 115 + for t in types { 116 + count_type_references(t, counts); 117 + } 118 + } 119 + Type::Object { fields, .. } => { 120 + for field in fields { 121 + count_type_references(&field.ty, counts); 122 + } 123 + } 124 + Type::Constrained { base, .. } => count_type_references(base, counts), 125 + _ => {} 126 + } 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 + } 132 + 133 + fn extract_docs(docs: &[DocComment]) -> String { 134 + docs.iter() 135 + .map(|d| d.text.trim()) 136 + .collect::<Vec<_>>() 137 + .join("\n") 138 + } 139 + 140 + fn generate_record_json(record: &Record, usage_counts: &HashMap<String, usize>) -> Value { 141 + let mut required = Vec::new(); 142 + let mut properties = Map::new(); 143 + 144 + for field in &record.fields { 145 + if !field.optional { 146 + required.push(field.name.name.clone()); 147 + } 148 + 149 + let field_json = generate_type_json(&field.ty, usage_counts); 150 + properties.insert(field.name.name.clone(), field_json); 151 + } 152 + 153 + let record_obj = json!({ 154 + "type": "object", 155 + "required": required, 156 + "properties": properties 157 + }); 158 + 159 + json!({ 160 + "type": "record", 161 + "description": extract_docs(&record.docs), 162 + "key": "tid", 163 + "record": record_obj 164 + }) 165 + } 166 + 167 + fn generate_query_json(query: &Query, usage_counts: &HashMap<String, usize>) -> Value { 168 + let mut params_properties = Map::new(); 169 + let mut params_required = Vec::new(); 170 + 171 + for param in &query.params { 172 + if !param.optional { 173 + params_required.push(param.name.name.clone()); 174 + } 175 + let param_json = generate_type_json(&param.ty, usage_counts); 176 + params_properties.insert(param.name.name.clone(), param_json); 177 + } 178 + 179 + let params = if !params_properties.is_empty() { 180 + json!({ 181 + "type": "params", 182 + "required": params_required, 183 + "properties": params_properties 184 + }) 185 + } else { 186 + json!({ 187 + "type": "params", 188 + "properties": {} 189 + }) 190 + }; 191 + 192 + let output = match &query.returns { 193 + ReturnType::Type(ty) => { 194 + json!({ 195 + "encoding": "application/json", 196 + "schema": generate_type_json(ty, usage_counts) 197 + }) 198 + } 199 + ReturnType::TypeWithErrors { success, errors, .. } => { 200 + let mut error_defs = Map::new(); 201 + for error in errors { 202 + error_defs.insert( 203 + error.name.name.clone(), 204 + json!({ 205 + "description": extract_docs(&error.docs) 206 + }), 207 + ); 208 + } 209 + 210 + json!({ 211 + "encoding": "application/json", 212 + "schema": generate_type_json(success, usage_counts), 213 + "errors": error_defs 214 + }) 215 + } 216 + }; 217 + 218 + json!({ 219 + "type": "query", 220 + "description": extract_docs(&query.docs), 221 + "parameters": params, 222 + "output": output 223 + }) 224 + } 225 + 226 + fn generate_procedure_json(procedure: &Procedure, usage_counts: &HashMap<String, usize>) -> Value { 227 + let mut params_properties = Map::new(); 228 + let mut params_required = Vec::new(); 229 + 230 + for param in &procedure.params { 231 + if !param.optional { 232 + params_required.push(param.name.name.clone()); 233 + } 234 + let param_json = generate_type_json(&param.ty, usage_counts); 235 + params_properties.insert(param.name.name.clone(), param_json); 236 + } 237 + 238 + 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 + }) 247 + } else { 248 + Value::Null 249 + }; 250 + 251 + let output = match &procedure.returns { 252 + ReturnType::Type(ty) => { 253 + json!({ 254 + "encoding": "application/json", 255 + "schema": generate_type_json(ty, usage_counts) 256 + }) 257 + } 258 + ReturnType::TypeWithErrors { success, errors, .. } => { 259 + let mut error_defs = Map::new(); 260 + for error in errors { 261 + error_defs.insert( 262 + error.name.name.clone(), 263 + json!({ 264 + "description": extract_docs(&error.docs) 265 + }), 266 + ); 267 + } 268 + 269 + json!({ 270 + "encoding": "application/json", 271 + "schema": generate_type_json(success, usage_counts), 272 + "errors": error_defs 273 + }) 274 + } 275 + }; 276 + 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; 285 + } 286 + 287 + result 288 + } 289 + 290 + fn generate_subscription_json( 291 + subscription: &Subscription, 292 + usage_counts: &HashMap<String, usize>, 293 + ) -> Value { 294 + let mut params_properties = Map::new(); 295 + let mut params_required = Vec::new(); 296 + 297 + for param in &subscription.params { 298 + if !param.optional { 299 + params_required.push(param.name.name.clone()); 300 + } 301 + let param_json = generate_type_json(&param.ty, usage_counts); 302 + params_properties.insert(param.name.name.clone(), param_json); 303 + } 304 + 305 + let parameters = if !params_properties.is_empty() { 306 + json!({ 307 + "type": "params", 308 + "required": params_required, 309 + "properties": params_properties 310 + }) 311 + } else { 312 + Value::Null 313 + }; 314 + 315 + let message = json!({ 316 + "schema": generate_type_json(&subscription.messages, usage_counts) 317 + }); 318 + 319 + let mut result = json!({ 320 + "type": "subscription", 321 + "description": extract_docs(&subscription.docs), 322 + "message": message 323 + }); 324 + 325 + if !parameters.is_null() { 326 + result["parameters"] = parameters; 327 + } 328 + 329 + result 330 + } 331 + 332 + fn generate_alias_json(alias: &Alias, usage_counts: &HashMap<String, usize>) -> Value { 333 + generate_type_json(&alias.ty, usage_counts) 334 + } 335 + 336 + fn generate_type_json(ty: &Type, usage_counts: &HashMap<String, usize>) -> Value { 337 + match ty { 338 + Type::Primitive { kind, .. } => generate_primitive_json(*kind), 339 + Type::Reference { path, .. } => { 340 + if path.segments.len() == 1 { 341 + 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 + } 347 + } else { 348 + json!({ "ref": path.to_string() }) 349 + } 350 + } 351 + Type::Array { inner, .. } => { 352 + json!({ 353 + "type": "array", 354 + "items": generate_type_json(inner, usage_counts) 355 + }) 356 + } 357 + Type::Union { types, .. } => { 358 + let refs: Vec<Value> = types 359 + .iter() 360 + .map(|t| generate_type_json(t, usage_counts)) 361 + .collect(); 362 + json!({ 363 + "type": "union", 364 + "refs": refs 365 + }) 366 + } 367 + Type::Object { fields, .. } => { 368 + let mut required = Vec::new(); 369 + let mut properties = Map::new(); 370 + 371 + for field in fields { 372 + if !field.optional { 373 + required.push(field.name.name.clone()); 374 + } 375 + properties.insert( 376 + field.name.name.clone(), 377 + generate_type_json(&field.ty, usage_counts), 378 + ); 379 + } 380 + 381 + json!({ 382 + "type": "object", 383 + "required": required, 384 + "properties": properties 385 + }) 386 + } 387 + Type::Constrained { base, constraints, .. } => { 388 + let mut base_json = generate_type_json(base, usage_counts); 389 + 390 + if let Some(obj) = base_json.as_object_mut() { 391 + for constraint in constraints { 392 + apply_constraint_to_json(obj, constraint); 393 + } 394 + } 395 + 396 + base_json 397 + } 398 + Type::Unknown { .. } => { 399 + json!({ "type": "unknown" }) 400 + } 401 + } 402 + } 403 + 404 + fn generate_primitive_json(kind: PrimitiveType) -> Value { 405 + match kind { 406 + PrimitiveType::Null => json!({ "type": "null" }), 407 + PrimitiveType::Boolean => json!({ "type": "boolean" }), 408 + PrimitiveType::Integer => json!({ "type": "integer" }), 409 + PrimitiveType::Number => json!({ "type": "number" }), 410 + PrimitiveType::String => json!({ "type": "string" }), 411 + PrimitiveType::Bytes => json!({ "type": "bytes" }), 412 + PrimitiveType::Blob => json!({ "type": "blob" }), 413 + } 414 + } 415 + 416 + fn apply_constraint_to_json(obj: &mut Map<String, Value>, constraint: &Constraint) { 417 + match constraint { 418 + Constraint::MinLength { value, .. } => { 419 + obj.insert("minLength".to_string(), json!(value)); 420 + } 421 + Constraint::MaxLength { value, .. } => { 422 + obj.insert("maxLength".to_string(), json!(value)); 423 + } 424 + Constraint::MinGraphemes { value, .. } => { 425 + obj.insert("minGraphemes".to_string(), json!(value)); 426 + } 427 + Constraint::MaxGraphemes { value, .. } => { 428 + obj.insert("maxGraphemes".to_string(), json!(value)); 429 + } 430 + Constraint::Minimum { value, .. } => { 431 + obj.insert("minimum".to_string(), json!(value)); 432 + } 433 + Constraint::Maximum { value, .. } => { 434 + obj.insert("maximum".to_string(), json!(value)); 435 + } 436 + Constraint::Format { value, .. } => { 437 + obj.insert("format".to_string(), json!(value)); 438 + } 439 + Constraint::Enum { values, .. } => { 440 + obj.insert("enum".to_string(), json!(values)); 441 + } 442 + Constraint::KnownValues { values, .. } => { 443 + let known_vals: Vec<String> = values.iter().map(|path| path.to_string()).collect(); 444 + obj.insert("knownValues".to_string(), json!(known_vals)); 445 + } 446 + Constraint::Accept { mimes, .. } => { 447 + obj.insert("accept".to_string(), json!(mimes)); 448 + } 449 + Constraint::MaxSize { value, .. } => { 450 + obj.insert("maxSize".to_string(), json!(value)); 451 + } 452 + Constraint::Default { value, .. } => { 453 + let default_val = match value { 454 + ConstraintValue::String(s) => json!(s), 455 + ConstraintValue::Integer(i) => json!(i), 456 + ConstraintValue::Boolean(b) => json!(b), 457 + }; 458 + obj.insert("default".to_string(), default_val); 459 + } 460 + } 461 + }
+8
mlf-diagnostics/Cargo.toml
··· 1 + [package] 2 + name = "mlf-diagnostics" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + mlf-lang = { path = "../mlf-lang" } 8 + miette = { version = "7", features = ["fancy"] }
+237
mlf-diagnostics/src/lib.rs
··· 1 + use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode}; 2 + use mlf_lang::{ParseError, ValidationError, ValidationErrors}; 3 + use std::fmt; 4 + 5 + #[derive(Debug)] 6 + pub struct ParseDiagnostic { 7 + source_code: NamedSource<String>, 8 + error: ParseError, 9 + } 10 + 11 + impl ParseDiagnostic { 12 + pub fn new(filename: String, source: String, error: ParseError) -> Self { 13 + Self { 14 + source_code: NamedSource::new(filename, source), 15 + error, 16 + } 17 + } 18 + } 19 + 20 + impl fmt::Display for ParseDiagnostic { 21 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 + match &self.error { 23 + ParseError::Syntax { message, .. } => write!(f, "Syntax error: {}", message), 24 + ParseError::UnexpectedEof { expected, .. } => { 25 + write!(f, "Unexpected end of file, expected {}", expected) 26 + } 27 + ParseError::InvalidIdentifier { name, .. } => { 28 + write!(f, "Invalid identifier: {}", name) 29 + } 30 + } 31 + } 32 + } 33 + 34 + impl std::error::Error for ParseDiagnostic {} 35 + 36 + impl Diagnostic for ParseDiagnostic { 37 + fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> { 38 + Some(Box::new("mlf::parse")) 39 + } 40 + 41 + fn source_code(&self) -> Option<&dyn SourceCode> { 42 + Some(&self.source_code) 43 + } 44 + 45 + fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> { 46 + let span = match &self.error { 47 + ParseError::Syntax { span, .. } => *span, 48 + ParseError::UnexpectedEof { span, .. } => *span, 49 + ParseError::InvalidIdentifier { span, .. } => *span, 50 + }; 51 + 52 + Some(Box::new(std::iter::once( 53 + LabeledSpan::at(span.start..span.end, "here"), 54 + ))) 55 + } 56 + 57 + fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> { 58 + match &self.error { 59 + ParseError::Syntax { message, .. } if message.contains("Expected") => { 60 + Some(Box::new("Check for missing commas, semicolons, or braces")) 61 + } 62 + ParseError::InvalidIdentifier { .. } => Some(Box::new( 63 + "Identifiers must start with a letter and contain only letters, numbers, and underscores", 64 + )), 65 + _ => None, 66 + } 67 + } 68 + } 69 + 70 + #[derive(Debug)] 71 + pub struct ValidationDiagnostic { 72 + source_code: NamedSource<String>, 73 + errors: ValidationErrors, 74 + } 75 + 76 + impl ValidationDiagnostic { 77 + pub fn new(filename: String, source: String, errors: ValidationErrors) -> Self { 78 + Self { 79 + source_code: NamedSource::new(filename, source), 80 + errors, 81 + } 82 + } 83 + } 84 + 85 + impl fmt::Display for ValidationDiagnostic { 86 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 + if self.errors.len() == 1 { 88 + format_validation_error(f, &self.errors.errors[0]) 89 + } else { 90 + write!(f, "Found {} validation errors", self.errors.len()) 91 + } 92 + } 93 + } 94 + 95 + impl std::error::Error for ValidationDiagnostic {} 96 + 97 + impl Diagnostic for ValidationDiagnostic { 98 + fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> { 99 + if self.errors.len() == 1 { 100 + Some(Box::new(get_error_code(&self.errors.errors[0]))) 101 + } else { 102 + Some(Box::new("mlf::validation")) 103 + } 104 + } 105 + 106 + fn source_code(&self) -> Option<&dyn SourceCode> { 107 + Some(&self.source_code) 108 + } 109 + 110 + fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> { 111 + let labels: Vec<LabeledSpan> = self 112 + .errors 113 + .errors 114 + .iter() 115 + .flat_map(|error| get_error_labels(error)) 116 + .collect(); 117 + 118 + if labels.is_empty() { 119 + None 120 + } else { 121 + Some(Box::new(labels.into_iter())) 122 + } 123 + } 124 + 125 + fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> { 126 + if self.errors.len() == 1 { 127 + get_error_help(&self.errors.errors[0]) 128 + } else { 129 + None 130 + } 131 + } 132 + } 133 + 134 + fn format_validation_error(f: &mut fmt::Formatter<'_>, error: &ValidationError) -> fmt::Result { 135 + match error { 136 + ValidationError::DuplicateDefinition { name, .. } => { 137 + write!(f, "Duplicate definition of '{}'", name) 138 + } 139 + ValidationError::UndefinedReference { name, .. } => { 140 + write!(f, "Undefined reference to '{}'", name) 141 + } 142 + ValidationError::InvalidConstraint { message, .. } => { 143 + write!(f, "Invalid constraint: {}", message) 144 + } 145 + ValidationError::TypeMismatch { 146 + expected, found, .. 147 + } => { 148 + write!(f, "Type mismatch: expected {}, found {}", expected, found) 149 + } 150 + ValidationError::ConstraintTooPermissive { message, .. } => { 151 + write!(f, "Constraint is too permissive: {}", message) 152 + } 153 + } 154 + } 155 + 156 + fn get_error_code(error: &ValidationError) -> &'static str { 157 + match error { 158 + ValidationError::DuplicateDefinition { .. } => "mlf::duplicate_definition", 159 + ValidationError::UndefinedReference { .. } => "mlf::undefined_reference", 160 + ValidationError::InvalidConstraint { .. } => "mlf::invalid_constraint", 161 + ValidationError::TypeMismatch { .. } => "mlf::type_mismatch", 162 + ValidationError::ConstraintTooPermissive { .. } => "mlf::constraint_too_permissive", 163 + } 164 + } 165 + 166 + fn get_error_labels(error: &ValidationError) -> Vec<LabeledSpan> { 167 + match error { 168 + ValidationError::DuplicateDefinition { 169 + first_span, 170 + second_span, 171 + name, 172 + } => vec![ 173 + LabeledSpan::at( 174 + first_span.start..first_span.end, 175 + format!("'{}' first defined here", name), 176 + ), 177 + LabeledSpan::at( 178 + second_span.start..second_span.end, 179 + format!("'{}' redefined here", name), 180 + ), 181 + ], 182 + ValidationError::UndefinedReference { span, name } => vec![LabeledSpan::at( 183 + span.start..span.end, 184 + format!("'{}' is not defined", name), 185 + )], 186 + ValidationError::InvalidConstraint { span, message } => { 187 + vec![LabeledSpan::at(span.start..span.end, message.clone())] 188 + } 189 + ValidationError::TypeMismatch { span, .. } => { 190 + vec![LabeledSpan::at(span.start..span.end, "type mismatch here")] 191 + } 192 + ValidationError::ConstraintTooPermissive { span, message } => { 193 + vec![LabeledSpan::at(span.start..span.end, message.clone())] 194 + } 195 + } 196 + } 197 + 198 + fn get_error_help(error: &ValidationError) -> Option<Box<dyn fmt::Display>> { 199 + match error { 200 + ValidationError::UndefinedReference { name, .. } => { 201 + if name.chars().next().map_or(false, |c| c.is_uppercase()) { 202 + Some(Box::new( 203 + "Did you forget to import this type? Try adding a 'use' statement, or check that it's defined in the prelude.", 204 + )) 205 + } else { 206 + Some(Box::new( 207 + "Make sure this type is defined in the same file or imported via 'use'.", 208 + )) 209 + } 210 + } 211 + ValidationError::InvalidConstraint { message, .. } 212 + if message.contains("String constraint on non-string") => 213 + { 214 + Some(Box::new( 215 + "String constraints (maxLength, minLength, format, enum) can only be applied to string types.", 216 + )) 217 + } 218 + ValidationError::InvalidConstraint { message, .. } 219 + if message.contains("Numeric constraint on non-numeric") => 220 + { 221 + Some(Box::new( 222 + "Numeric constraints (minimum, maximum) can only be applied to integer or number types.", 223 + )) 224 + } 225 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("maxLength") => { 226 + Some(Box::new( 227 + "When refining a constrained type, maxLength can only decrease (become more restrictive).", 228 + )) 229 + } 230 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("minLength") => { 231 + Some(Box::new( 232 + "When refining a constrained type, minLength can only increase (become more restrictive).", 233 + )) 234 + } 235 + _ => None, 236 + } 237 + }
+25
mlf-lang/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "memchr" 7 + version = "2.7.6" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 10 + 11 + [[package]] 12 + name = "mlf" 13 + version = "0.1.0" 14 + dependencies = [ 15 + "nom", 16 + ] 17 + 18 + [[package]] 19 + name = "nom" 20 + version = "8.0.0" 21 + source = "registry+https://github.com/rust-lang/crates.io-index" 22 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 23 + dependencies = [ 24 + "memchr", 25 + ]
+11
mlf-lang/Cargo.toml
··· 1 + [package] 2 + name = "mlf-lang" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + nom = { version = "8", default-features = false, features = ["alloc"] } 8 + 9 + [features] 10 + default = ["std"] 11 + std = ["nom/std"]
+329
mlf-lang/src/ast.rs
··· 1 + use alloc::string::String; 2 + use alloc::vec::Vec; 3 + 4 + use crate::span::{Span, Spanned}; 5 + #[derive(Debug, Clone, PartialEq, Eq)] 6 + pub struct Ident { 7 + pub name: String, 8 + pub span: Span, 9 + } 10 + 11 + impl Ident { 12 + pub fn new(name: String, span: Span) -> Self { 13 + Self { name, span } 14 + } 15 + } 16 + 17 + impl Spanned for Ident { 18 + fn span(&self) -> Span { 19 + self.span 20 + } 21 + } 22 + 23 + /// A complete lexicon (top-level AST node) 24 + #[derive(Debug, Clone, PartialEq)] 25 + pub struct Lexicon { 26 + pub items: Vec<Item>, 27 + pub span: Span, 28 + } 29 + 30 + /// Top-level items in a lexicon 31 + #[derive(Debug, Clone, PartialEq)] 32 + pub enum Item { 33 + Record(Record), 34 + Alias(Alias), 35 + Token(Token), 36 + Query(Query), 37 + Procedure(Procedure), 38 + Subscription(Subscription), 39 + Namespace(Namespace), 40 + Use(Use), 41 + } 42 + 43 + impl Spanned for Item { 44 + fn span(&self) -> Span { 45 + match self { 46 + Item::Record(r) => r.span, 47 + Item::Alias(a) => a.span, 48 + Item::Token(t) => t.span, 49 + Item::Query(q) => q.span, 50 + Item::Procedure(p) => p.span, 51 + Item::Subscription(s) => s.span, 52 + Item::Namespace(n) => n.span, 53 + Item::Use(u) => u.span, 54 + } 55 + } 56 + } 57 + 58 + /// Documentation comment 59 + #[derive(Debug, Clone, PartialEq, Eq)] 60 + pub struct DocComment { 61 + pub text: String, 62 + pub span: Span, 63 + } 64 + 65 + /// Annotation (e.g., @deprecated, @since(1, 0, 0)) 66 + #[derive(Debug, Clone, PartialEq)] 67 + pub struct Annotation { 68 + pub name: Ident, 69 + pub args: Vec<AnnotationArg>, 70 + pub span: Span, 71 + } 72 + 73 + #[derive(Debug, Clone, PartialEq)] 74 + pub enum AnnotationArg { 75 + /// Positional argument 76 + Positional(AnnotationValue), 77 + /// Named argument 78 + Named { name: Ident, value: AnnotationValue }, 79 + } 80 + 81 + #[derive(Debug, Clone, PartialEq)] 82 + pub enum AnnotationValue { 83 + String(String), 84 + Number(f64), 85 + Boolean(bool), 86 + } 87 + 88 + /// A record definition 89 + #[derive(Debug, Clone, PartialEq)] 90 + pub struct Record { 91 + pub docs: Vec<DocComment>, 92 + pub annotations: Vec<Annotation>, 93 + pub name: Ident, 94 + pub fields: Vec<Field>, 95 + pub span: Span, 96 + } 97 + 98 + /// A field in a record or alias 99 + #[derive(Debug, Clone, PartialEq)] 100 + pub struct Field { 101 + pub docs: Vec<DocComment>, 102 + pub annotations: Vec<Annotation>, 103 + pub name: Ident, 104 + pub ty: Type, 105 + pub optional: bool, 106 + pub span: Span, 107 + } 108 + 109 + /// A type alias 110 + #[derive(Debug, Clone, PartialEq)] 111 + pub struct Alias { 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 token definition 120 + #[derive(Debug, Clone, PartialEq)] 121 + pub struct Token { 122 + pub docs: Vec<DocComment>, 123 + pub annotations: Vec<Annotation>, 124 + pub name: Ident, 125 + pub span: Span, 126 + } 127 + 128 + /// A query definition 129 + #[derive(Debug, Clone, PartialEq)] 130 + pub struct Query { 131 + pub docs: Vec<DocComment>, 132 + pub annotations: Vec<Annotation>, 133 + pub name: Ident, 134 + pub params: Vec<Field>, 135 + pub returns: ReturnType, 136 + pub span: Span, 137 + } 138 + 139 + /// A procedure definition 140 + #[derive(Debug, Clone, PartialEq)] 141 + pub struct Procedure { 142 + pub docs: Vec<DocComment>, 143 + pub annotations: Vec<Annotation>, 144 + pub name: Ident, 145 + pub params: Vec<Field>, 146 + pub returns: ReturnType, 147 + pub span: Span, 148 + } 149 + 150 + /// A subscription definition 151 + #[derive(Debug, Clone, PartialEq)] 152 + pub struct Subscription { 153 + pub docs: Vec<DocComment>, 154 + pub annotations: Vec<Annotation>, 155 + pub name: Ident, 156 + pub params: Vec<Field>, 157 + pub messages: Type, // Union of message types 158 + pub span: Span, 159 + } 160 + 161 + /// Return type for queries and procedures 162 + #[derive(Debug, Clone, PartialEq)] 163 + pub enum ReturnType { 164 + /// Simple return type 165 + Type(Type), 166 + /// Return type with error handling 167 + TypeWithErrors { 168 + success: Type, 169 + errors: Vec<ErrorDef>, 170 + span: Span, 171 + }, 172 + } 173 + 174 + /// An error definition in a query/procedure 175 + #[derive(Debug, Clone, PartialEq)] 176 + pub struct ErrorDef { 177 + pub docs: Vec<DocComment>, 178 + pub name: Ident, 179 + pub span: Span, 180 + } 181 + 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 + /// A use statement 191 + #[derive(Debug, Clone, PartialEq)] 192 + pub struct Use { 193 + pub path: Path, 194 + pub imports: UseImports, 195 + pub span: Span, 196 + } 197 + 198 + /// What to import from a path 199 + #[derive(Debug, Clone, PartialEq)] 200 + pub enum UseImports { 201 + /// Import all (*) 202 + All, 203 + /// Import specific items 204 + Items(Vec<UseItem>), 205 + } 206 + 207 + /// An individual use item 208 + #[derive(Debug, Clone, PartialEq)] 209 + pub struct UseItem { 210 + pub name: Ident, 211 + pub alias: Option<Ident>, 212 + } 213 + 214 + /// A dotted path (e.g., app.bsky.feed.post) 215 + #[derive(Debug, Clone, PartialEq, Eq)] 216 + pub struct Path { 217 + pub segments: Vec<Ident>, 218 + pub span: Span, 219 + } 220 + 221 + impl Path { 222 + pub fn to_string(&self) -> String { 223 + self.segments 224 + .iter() 225 + .map(|s| s.name.as_str()) 226 + .collect::<Vec<_>>() 227 + .join(".") 228 + } 229 + } 230 + 231 + /// A type expression 232 + #[derive(Debug, Clone, PartialEq)] 233 + pub enum Type { 234 + /// Primitive type (string, integer, boolean, etc.) 235 + Primitive { kind: PrimitiveType, span: Span }, 236 + /// Reference to another type (local or cross-file) 237 + Reference { path: Path, span: Span }, 238 + /// Array type 239 + Array { inner: Box<Type>, span: Span }, 240 + /// Union type 241 + Union { types: Vec<Type>, span: Span }, 242 + /// Object type (inline) 243 + Object { fields: Vec<Field>, span: Span }, 244 + /// Constrained type 245 + Constrained { 246 + base: Box<Type>, 247 + constraints: Vec<Constraint>, 248 + span: Span, 249 + }, 250 + /// Unknown type 251 + Unknown { span: Span }, 252 + } 253 + 254 + impl Spanned for Type { 255 + fn span(&self) -> Span { 256 + match self { 257 + Type::Primitive { span, .. } => *span, 258 + Type::Reference { span, .. } => *span, 259 + Type::Array { span, .. } => *span, 260 + Type::Union { span, .. } => *span, 261 + Type::Object { span, .. } => *span, 262 + Type::Constrained { span, .. } => *span, 263 + Type::Unknown { span } => *span, 264 + } 265 + } 266 + } 267 + 268 + /// Primitive types 269 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 270 + pub enum PrimitiveType { 271 + Null, 272 + Boolean, 273 + Integer, 274 + Number, 275 + String, 276 + Bytes, 277 + Blob, 278 + } 279 + 280 + /// Type constraints 281 + #[derive(Debug, Clone, PartialEq)] 282 + pub enum Constraint { 283 + // String constraints 284 + MinLength { value: usize, span: Span }, 285 + MaxLength { value: usize, span: Span }, 286 + MinGraphemes { value: usize, span: Span }, 287 + MaxGraphemes { value: usize, span: Span }, 288 + Format { value: String, span: Span }, 289 + Enum { values: Vec<String>, span: Span }, 290 + KnownValues { values: Vec<Path>, span: Span }, 291 + 292 + // Numeric constraints 293 + Minimum { value: i64, span: Span }, 294 + Maximum { value: i64, span: Span }, 295 + 296 + // Blob constraints 297 + Accept { mimes: Vec<String>, span: Span }, 298 + MaxSize { value: usize, span: Span }, 299 + 300 + // Default value 301 + Default { value: ConstraintValue, span: Span }, 302 + } 303 + 304 + impl Spanned for Constraint { 305 + fn span(&self) -> Span { 306 + match self { 307 + Constraint::MinLength { span, .. } => *span, 308 + Constraint::MaxLength { span, .. } => *span, 309 + Constraint::MinGraphemes { span, .. } => *span, 310 + Constraint::MaxGraphemes { span, .. } => *span, 311 + Constraint::Format { span, .. } => *span, 312 + Constraint::Enum { span, .. } => *span, 313 + Constraint::KnownValues { span, .. } => *span, 314 + Constraint::Minimum { span, .. } => *span, 315 + Constraint::Maximum { span, .. } => *span, 316 + Constraint::Accept { span, .. } => *span, 317 + Constraint::MaxSize { span, .. } => *span, 318 + Constraint::Default { span, .. } => *span, 319 + } 320 + } 321 + } 322 + 323 + /// A value in a constraint default 324 + #[derive(Debug, Clone, PartialEq)] 325 + pub enum ConstraintValue { 326 + String(String), 327 + Integer(i64), 328 + Boolean(bool), 329 + }
+47
mlf-lang/src/error.rs
··· 1 + use alloc::string::String; 2 + use alloc::vec::Vec; 3 + 4 + use crate::span::Span; 5 + 6 + #[derive(Debug, Clone, PartialEq)] 7 + pub enum ParseError { 8 + Syntax { message: String, span: Span }, 9 + UnexpectedEof { expected: String, span: Span }, 10 + InvalidIdentifier { name: String, span: Span }, 11 + } 12 + 13 + #[derive(Debug, Clone, PartialEq)] 14 + pub enum ValidationError { 15 + DuplicateDefinition { name: String, first_span: Span, second_span: Span }, 16 + UndefinedReference { name: String, span: Span }, 17 + InvalidConstraint { message: String, span: Span }, 18 + TypeMismatch { expected: String, found: String, span: Span }, 19 + ConstraintTooPermissive { message: String, span: Span }, 20 + } 21 + 22 + #[derive(Debug, Clone, Default)] 23 + pub struct ValidationErrors { 24 + pub errors: Vec<ValidationError>, 25 + } 26 + 27 + impl ValidationErrors { 28 + pub fn new() -> Self { 29 + Self { errors: Vec::new() } 30 + } 31 + 32 + pub fn push(&mut self, error: ValidationError) { 33 + self.errors.push(error); 34 + } 35 + 36 + pub fn append(&mut self, other: &mut Self) { 37 + self.errors.append(&mut other.errors); 38 + } 39 + 40 + pub fn is_empty(&self) -> bool { 41 + self.errors.is_empty() 42 + } 43 + 44 + pub fn len(&self) -> usize { 45 + self.errors.len() 46 + } 47 + }
+418
mlf-lang/src/lexer.rs
··· 1 + use alloc::string::String; 2 + use alloc::vec::Vec; 3 + use nom::{ 4 + branch::alt, 5 + bytes::complete::{tag, take_while, take_while1}, 6 + character::complete::{char, multispace0, one_of}, 7 + combinator::{map, opt, recognize}, 8 + multi::many0, 9 + sequence::{delimited, pair, preceded}, 10 + IResult, Parser, 11 + }; 12 + 13 + use crate::span::Span; 14 + 15 + #[derive(Debug, Clone, PartialEq)] 16 + pub enum Token { 17 + // Keywords 18 + Alias, 19 + As, 20 + Blob, 21 + Boolean, 22 + Bytes, 23 + Constrained, 24 + Error, 25 + Integer, 26 + Namespace, 27 + Null, 28 + Number, 29 + Procedure, 30 + Query, 31 + Record, 32 + String, 33 + Subscription, 34 + Token, 35 + Unknown, 36 + Use, 37 + 38 + // Identifiers and literals 39 + Ident(String), 40 + StringLit(String), 41 + IntLit(i64), 42 + FloatLit(f64), 43 + True, 44 + False, 45 + 46 + // Symbols 47 + Colon, 48 + Comma, 49 + Dot, 50 + Pipe, 51 + Question, 52 + At, 53 + Equals, 54 + Semicolon, 55 + LeftBrace, 56 + RightBrace, 57 + LeftBracket, 58 + RightBracket, 59 + LeftParen, 60 + RightParen, 61 + Underscore, 62 + 63 + // Special 64 + DocComment(String), 65 + Eof, 66 + } 67 + 68 + impl core::fmt::Display for Token { 69 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 70 + match self { 71 + Token::Alias => write!(f, "alias"), 72 + Token::As => write!(f, "as"), 73 + Token::Blob => write!(f, "blob"), 74 + Token::Boolean => write!(f, "boolean"), 75 + Token::Bytes => write!(f, "bytes"), 76 + Token::Constrained => write!(f, "constrained"), 77 + Token::Error => write!(f, "error"), 78 + Token::Integer => write!(f, "integer"), 79 + Token::Namespace => write!(f, "namespace"), 80 + Token::Null => write!(f, "null"), 81 + Token::Number => write!(f, "number"), 82 + Token::Procedure => write!(f, "procedure"), 83 + Token::Query => write!(f, "query"), 84 + Token::Record => write!(f, "record"), 85 + Token::String => write!(f, "string"), 86 + Token::Subscription => write!(f, "subscription"), 87 + Token::Token => write!(f, "token"), 88 + Token::Unknown => write!(f, "unknown"), 89 + Token::Use => write!(f, "use"), 90 + Token::Ident(s) => write!(f, "{}", s), 91 + Token::StringLit(s) => write!(f, "\"{}\"", s), 92 + Token::IntLit(i) => write!(f, "{}", i), 93 + Token::FloatLit(fl) => write!(f, "{}", fl), 94 + Token::True => write!(f, "true"), 95 + Token::False => write!(f, "false"), 96 + Token::Colon => write!(f, ":"), 97 + Token::Comma => write!(f, ","), 98 + Token::Dot => write!(f, "."), 99 + Token::Pipe => write!(f, "|"), 100 + Token::Question => write!(f, "?"), 101 + Token::At => write!(f, "@"), 102 + Token::Equals => write!(f, "="), 103 + Token::Semicolon => write!(f, ";"), 104 + Token::LeftBrace => write!(f, "{{"), 105 + Token::RightBrace => write!(f, "}}"), 106 + Token::LeftBracket => write!(f, "["), 107 + Token::RightBracket => write!(f, "]"), 108 + Token::LeftParen => write!(f, "("), 109 + Token::RightParen => write!(f, ")"), 110 + Token::Underscore => write!(f, "_"), 111 + Token::DocComment(_) => write!(f, "doc comment"), 112 + Token::Eof => write!(f, "end of file"), 113 + } 114 + } 115 + } 116 + 117 + #[derive(Debug, Clone, PartialEq)] 118 + pub struct SpannedToken { 119 + pub token: Token, 120 + pub span: Span, 121 + } 122 + 123 + fn is_ident_start(c: char) -> bool { 124 + c.is_ascii_lowercase() || c == '_' 125 + } 126 + 127 + fn is_ident_continue(c: char) -> bool { 128 + c.is_ascii_alphanumeric() || c == '_' || c == '-' 129 + } 130 + 131 + fn is_type_start(c: char) -> bool { 132 + c.is_ascii_uppercase() 133 + } 134 + 135 + fn identifier(input: &str) -> IResult<&str, Token> { 136 + let (rest, name) = recognize(pair( 137 + take_while1(is_ident_start), 138 + take_while(is_ident_continue), 139 + )).parse(input)?; 140 + 141 + let token = match name { 142 + "alias" => Token::Alias, 143 + "as" => Token::As, 144 + "blob" => Token::Blob, 145 + "boolean" => Token::Boolean, 146 + "bytes" => Token::Bytes, 147 + "constrained" => Token::Constrained, 148 + "error" => Token::Error, 149 + "false" => Token::False, 150 + "integer" => Token::Integer, 151 + "namespace" => Token::Namespace, 152 + "null" => Token::Null, 153 + "number" => Token::Number, 154 + "procedure" => Token::Procedure, 155 + "query" => Token::Query, 156 + "record" => Token::Record, 157 + "string" => Token::String, 158 + "subscription" => Token::Subscription, 159 + "token" => Token::Token, 160 + "true" => Token::True, 161 + "unknown" => Token::Unknown, 162 + "use" => Token::Use, 163 + _ => Token::Ident(name.into()), 164 + }; 165 + 166 + Ok((rest, token)) 167 + } 168 + 169 + fn type_ident(input: &str) -> IResult<&str, Token> { 170 + let (rest, name) = recognize(pair( 171 + take_while1(is_type_start), 172 + take_while(is_ident_continue), 173 + )).parse(input)?; 174 + 175 + Ok((rest, Token::Ident(name.into()))) 176 + } 177 + 178 + fn raw_identifier(input: &str) -> IResult<&str, Token> { 179 + let (rest, name) = delimited( 180 + char('`'), 181 + take_while1(|c: char| c != '`'), 182 + char('`'), 183 + ).parse(input)?; 184 + 185 + Ok((rest, Token::Ident(name.into()))) 186 + } 187 + 188 + fn string_literal(input: &str) -> IResult<&str, Token> { 189 + let (rest, s) = delimited( 190 + char('"'), 191 + recognize(many0(alt(( 192 + take_while1(|c| c != '"' && c != '\\'), 193 + recognize(pair(char('\\'), one_of(r#""\/bfnrt"#))), 194 + )))), 195 + char('"'), 196 + ).parse(input)?; 197 + 198 + Ok((rest, Token::StringLit(s.into()))) 199 + } 200 + 201 + fn integer_literal(input: &str) -> IResult<&str, Token> { 202 + let (rest, num_str) = recognize(pair( 203 + opt(char('-')), 204 + take_while1(|c: char| c.is_ascii_digit()), 205 + )).parse(input)?; 206 + 207 + let num = num_str.parse::<i64>().unwrap(); 208 + Ok((rest, Token::IntLit(num))) 209 + } 210 + 211 + fn float_literal(input: &str) -> IResult<&str, Token> { 212 + let (rest, num_str) = recognize(( 213 + opt(char('-')), 214 + take_while1(|c: char| c.is_ascii_digit()), 215 + char('.'), 216 + take_while1(|c: char| c.is_ascii_digit()), 217 + )).parse(input)?; 218 + 219 + let num = num_str.parse::<f64>().unwrap(); 220 + Ok((rest, Token::FloatLit(num))) 221 + } 222 + 223 + fn doc_comment(input: &str) -> IResult<&str, Token> { 224 + let (rest, comment) = preceded( 225 + tag("///"), 226 + map( 227 + take_while(|c| c != '\n'), 228 + |s: &str| Token::DocComment(s.trim().into()), 229 + ), 230 + ).parse(input)?; 231 + 232 + Ok((rest, comment)) 233 + } 234 + 235 + fn line_comment(input: &str) -> IResult<&str, ()> { 236 + let (rest, _) = preceded( 237 + tag("//"), 238 + take_while(|c| c != '\n'), 239 + ).parse(input)?; 240 + 241 + Ok((rest, ())) 242 + } 243 + 244 + fn hash_comment(input: &str) -> IResult<&str, ()> { 245 + let (rest, _) = preceded( 246 + char('#'), 247 + take_while(|c| c != '\n'), 248 + ).parse(input)?; 249 + 250 + Ok((rest, ())) 251 + } 252 + 253 + fn symbol(input: &str) -> IResult<&str, Token> { 254 + alt(( 255 + map(char(':'), |_| Token::Colon), 256 + map(char(','), |_| Token::Comma), 257 + map(char('.'), |_| Token::Dot), 258 + map(char('|'), |_| Token::Pipe), 259 + map(char('?'), |_| Token::Question), 260 + map(char('@'), |_| Token::At), 261 + map(char('='), |_| Token::Equals), 262 + map(char(';'), |_| Token::Semicolon), 263 + map(char('{'), |_| Token::LeftBrace), 264 + map(char('}'), |_| Token::RightBrace), 265 + map(char('['), |_| Token::LeftBracket), 266 + map(char(']'), |_| Token::RightBracket), 267 + map(char('('), |_| Token::LeftParen), 268 + map(char(')'), |_| Token::RightParen), 269 + map(char('_'), |_| Token::Underscore), 270 + )).parse(input) 271 + } 272 + 273 + fn single_token(input: &str) -> IResult<&str, Option<Token>> { 274 + alt(( 275 + map(doc_comment, Some), 276 + map(line_comment, |_| None), 277 + map(hash_comment, |_| None), 278 + map(float_literal, Some), 279 + map(integer_literal, Some), 280 + map(string_literal, Some), 281 + map(raw_identifier, Some), 282 + map(type_ident, Some), 283 + map(identifier, Some), 284 + map(symbol, Some), 285 + )).parse(input) 286 + } 287 + 288 + pub fn tokenize(input: &str) -> Result<Vec<SpannedToken>, crate::error::ParseError> { 289 + let mut tokens = Vec::new(); 290 + let mut remaining = input; 291 + let mut pos = 0; 292 + 293 + while !remaining.is_empty() { 294 + 295 + let ws_result: IResult<&str, &str> = multispace0(remaining); 296 + if let Ok((rest, ws)) = ws_result { 297 + pos += ws.len(); 298 + remaining = rest; 299 + } 300 + 301 + if remaining.is_empty() { 302 + break; 303 + } 304 + 305 + match single_token(remaining) { 306 + Ok((rest, Some(token))) => { 307 + let consumed = remaining.len() - rest.len(); 308 + let span = Span::new(pos, pos + consumed); 309 + tokens.push(SpannedToken { token, span }); 310 + pos += consumed; 311 + remaining = rest; 312 + } 313 + Ok((rest, None)) => { 314 + let consumed = remaining.len() - rest.len(); 315 + pos += consumed; 316 + remaining = rest; 317 + } 318 + Err(_) => { 319 + return Err(crate::error::ParseError::Syntax { 320 + message: alloc::format!("Unexpected character: {:?}", remaining.chars().next()), 321 + span: Span::new(pos, pos + 1), 322 + }); 323 + } 324 + } 325 + } 326 + 327 + tokens.push(SpannedToken { 328 + token: Token::Eof, 329 + span: Span::new(pos, pos), 330 + }); 331 + 332 + Ok(tokens) 333 + } 334 + 335 + #[cfg(test)] 336 + mod tests { 337 + use super::*; 338 + 339 + #[test] 340 + fn test_keywords() { 341 + let input = "record alias query"; 342 + let tokens = tokenize(input).unwrap(); 343 + assert_eq!(tokens[0].token, Token::Record); 344 + assert_eq!(tokens[1].token, Token::Alias); 345 + assert_eq!(tokens[2].token, Token::Query); 346 + } 347 + 348 + #[test] 349 + fn test_identifiers() { 350 + let input = "foo bar_baz MyType"; 351 + let tokens = tokenize(input).unwrap(); 352 + assert_eq!(tokens[0].token, Token::Ident("foo".into())); 353 + assert_eq!(tokens[1].token, Token::Ident("bar_baz".into())); 354 + assert_eq!(tokens[2].token, Token::Ident("MyType".into())); 355 + } 356 + 357 + #[test] 358 + fn test_symbols() { 359 + let input = "{ } : , . | ? @"; 360 + let tokens = tokenize(input).unwrap(); 361 + assert_eq!(tokens[0].token, Token::LeftBrace); 362 + assert_eq!(tokens[1].token, Token::RightBrace); 363 + assert_eq!(tokens[2].token, Token::Colon); 364 + assert_eq!(tokens[3].token, Token::Comma); 365 + assert_eq!(tokens[4].token, Token::Dot); 366 + assert_eq!(tokens[5].token, Token::Pipe); 367 + assert_eq!(tokens[6].token, Token::Question); 368 + assert_eq!(tokens[7].token, Token::At); 369 + } 370 + 371 + #[test] 372 + fn test_doc_comment() { 373 + let input = "/// This is a doc comment"; 374 + let tokens = tokenize(input).unwrap(); 375 + assert_eq!(tokens[0].token, Token::DocComment("This is a doc comment".into())); 376 + } 377 + 378 + #[test] 379 + fn test_literals() { 380 + let input = r#"42 -10 3.14 "hello world" true false"#; 381 + let tokens = tokenize(input).unwrap(); 382 + assert_eq!(tokens[0].token, Token::IntLit(42)); 383 + assert_eq!(tokens[1].token, Token::IntLit(-10)); 384 + assert_eq!(tokens[2].token, Token::FloatLit(3.14)); 385 + assert_eq!(tokens[3].token, Token::StringLit("hello world".into())); 386 + assert_eq!(tokens[4].token, Token::True); 387 + assert_eq!(tokens[5].token, Token::False); 388 + } 389 + 390 + #[test] 391 + fn test_hash_comments() { 392 + let input = "# This is a comment\nrecord foo"; 393 + let tokens = tokenize(input).unwrap(); 394 + assert_eq!(tokens[0].token, Token::Record); 395 + assert_eq!(tokens[1].token, Token::Ident("foo".into())); 396 + } 397 + 398 + #[test] 399 + fn test_shebang() { 400 + let input = "#!/usr/bin/env mlf\nrecord foo"; 401 + let tokens = tokenize(input).unwrap(); 402 + assert_eq!(tokens[0].token, Token::Record); 403 + assert_eq!(tokens[1].token, Token::Ident("foo".into())); 404 + } 405 + 406 + #[test] 407 + fn test_mixed_comments() { 408 + let input = r#" 409 + // C++ style comment 410 + # Shell style comment 411 + /// Doc comment 412 + record foo"#; 413 + let tokens = tokenize(input).unwrap(); 414 + assert_eq!(tokens[0].token, Token::DocComment("Doc comment".into())); 415 + assert_eq!(tokens[1].token, Token::Record); 416 + assert_eq!(tokens[2].token, Token::Ident("foo".into())); 417 + } 418 + }
+19
mlf-lang/src/lib.rs
··· 1 + #![cfg_attr(not(feature = "std"), no_std)] 2 + 3 + extern crate alloc; 4 + 5 + pub mod ast; 6 + pub mod error; 7 + pub mod lexer; 8 + pub mod parser; 9 + pub mod span; 10 + pub mod validate; 11 + pub mod workspace; 12 + 13 + pub use ast::Lexicon; 14 + pub use error::{ParseError, ValidationError, ValidationErrors}; 15 + pub use parser::parse_lexicon; 16 + pub use validate::validate_lexicon; 17 + pub use workspace::Workspace; 18 + 19 + pub const PRELUDE: &str = include_str!("../../resources/prelude.mlf");
+1249
mlf-lang/src/parser.rs
··· 1 + use alloc::vec::Vec; 2 + use crate::{Lexicon, ParseError, ast::*, lexer::{tokenize, SpannedToken}, span::{Span, Spanned}}; 3 + use crate::lexer::Token as LexToken; 4 + 5 + struct Parser { 6 + tokens: Vec<SpannedToken>, 7 + pos: usize, 8 + } 9 + 10 + impl Parser { 11 + fn new(tokens: Vec<SpannedToken>) -> Self { 12 + Parser { tokens, pos: 0 } 13 + } 14 + 15 + fn current(&self) -> &SpannedToken { 16 + &self.tokens[self.pos] 17 + } 18 + 19 + fn peek(&self, offset: usize) -> Option<&SpannedToken> { 20 + self.tokens.get(self.pos + offset) 21 + } 22 + 23 + fn is_eof(&self) -> bool { 24 + matches!(self.current().token, LexToken::Eof) 25 + } 26 + 27 + fn advance(&mut self) -> &SpannedToken { 28 + let current = &self.tokens[self.pos]; 29 + if !self.is_eof() { 30 + self.pos += 1; 31 + } 32 + current 33 + } 34 + 35 + fn expect(&mut self, expected: LexToken) -> Result<Span, ParseError> { 36 + let current = self.current(); 37 + if current.token == expected { 38 + Ok(self.advance().span) 39 + } else { 40 + Err(ParseError::Syntax { 41 + message: alloc::format!("Expected {}, found {}", expected, current.token), 42 + span: current.span, 43 + }) 44 + } 45 + } 46 + 47 + fn parse_ident(&mut self) -> Result<Ident, ParseError> { 48 + let current = self.current(); 49 + if let LexToken::Ident(name) = &current.token { 50 + let ident = Ident { 51 + name: name.clone(), 52 + span: current.span, 53 + }; 54 + self.advance(); 55 + Ok(ident) 56 + } else { 57 + Err(ParseError::Syntax { 58 + message: alloc::format!("Expected identifier, found {}", current.token), 59 + span: current.span, 60 + }) 61 + } 62 + } 63 + 64 + fn parse_path(&mut self) -> Result<Path, ParseError> { 65 + let mut segments = Vec::new(); 66 + let start = self.current().span.start; 67 + 68 + segments.push(self.parse_ident()?); 69 + 70 + while matches!(self.current().token, LexToken::Dot) { 71 + self.advance(); 72 + segments.push(self.parse_ident()?); 73 + } 74 + 75 + let end = segments.last().unwrap().span.end; 76 + Ok(Path { 77 + segments, 78 + span: Span::new(start, end), 79 + }) 80 + } 81 + } 82 + 83 + pub fn parse_lexicon(input: &str) -> Result<Lexicon, ParseError> { 84 + let tokens = tokenize(input)?; 85 + let mut parser = Parser::new(tokens); 86 + let start = parser.current().span.start; 87 + 88 + let mut items = Vec::new(); 89 + 90 + while !parser.is_eof() { 91 + items.push(parser.parse_item()?); 92 + } 93 + 94 + let end = if let Some(last) = items.last() { 95 + last.span().end 96 + } else { 97 + start 98 + }; 99 + 100 + Ok(Lexicon { 101 + items, 102 + span: Span::new(start, end), 103 + }) 104 + } 105 + 106 + impl Parser { 107 + fn parse_item(&mut self) -> Result<Item, ParseError> { 108 + while matches!(self.current().token, LexToken::DocComment(_)) { 109 + self.advance(); 110 + } 111 + 112 + let annotations = self.parse_annotations()?; 113 + 114 + 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(), 122 + LexToken::Use => self.parse_use(), 123 + _ => Err(ParseError::Syntax { 124 + message: alloc::format!("Expected item definition, found {}", self.current().token), 125 + span: self.current().span, 126 + }), 127 + } 128 + } 129 + 130 + fn parse_annotations(&mut self) -> Result<Vec<Annotation>, ParseError> { 131 + let mut annotations = Vec::new(); 132 + 133 + while matches!(self.current().token, LexToken::At) { 134 + annotations.push(self.parse_annotation()?); 135 + } 136 + 137 + Ok(annotations) 138 + } 139 + 140 + fn parse_annotation(&mut self) -> Result<Annotation, ParseError> { 141 + let start = self.expect(LexToken::At)?; 142 + let name = self.parse_ident()?; 143 + 144 + let mut args = Vec::new(); 145 + 146 + if matches!(self.current().token, LexToken::LeftParen) { 147 + self.advance(); 148 + 149 + while !matches!(self.current().token, LexToken::RightParen) { 150 + args.push(self.parse_annotation_arg()?); 151 + 152 + if matches!(self.current().token, LexToken::Comma) { 153 + self.advance(); 154 + } else { 155 + break; 156 + } 157 + } 158 + 159 + self.expect(LexToken::RightParen)?; 160 + } 161 + 162 + let end = if args.is_empty() { 163 + name.span.end 164 + } else { 165 + self.tokens[self.pos - 1].span.end 166 + }; 167 + 168 + Ok(Annotation { 169 + name, 170 + args, 171 + span: Span::new(start.start, end), 172 + }) 173 + } 174 + 175 + fn parse_annotation_arg(&mut self) -> Result<AnnotationArg, ParseError> { 176 + if let Some(next) = self.peek(1) { 177 + if matches!(next.token, LexToken::Colon) { 178 + let name = self.parse_ident()?; 179 + self.expect(LexToken::Colon)?; 180 + let value = self.parse_annotation_value()?; 181 + return Ok(AnnotationArg::Named { name, value }); 182 + } 183 + } 184 + 185 + let value = self.parse_annotation_value()?; 186 + Ok(AnnotationArg::Positional(value)) 187 + } 188 + 189 + fn parse_annotation_value(&mut self) -> Result<AnnotationValue, ParseError> { 190 + let current = self.current(); 191 + match &current.token { 192 + LexToken::StringLit(s) => { 193 + let value = AnnotationValue::String(s.clone()); 194 + self.advance(); 195 + Ok(value) 196 + } 197 + LexToken::IntLit(i) => { 198 + let value = AnnotationValue::Number(*i as f64); 199 + self.advance(); 200 + Ok(value) 201 + } 202 + LexToken::FloatLit(f) => { 203 + let value = AnnotationValue::Number(*f); 204 + self.advance(); 205 + Ok(value) 206 + } 207 + LexToken::True => { 208 + self.advance(); 209 + Ok(AnnotationValue::Boolean(true)) 210 + } 211 + LexToken::False => { 212 + self.advance(); 213 + Ok(AnnotationValue::Boolean(false)) 214 + } 215 + _ => Err(ParseError::Syntax { 216 + message: alloc::format!("Expected annotation value, found {}", current.token), 217 + span: current.span, 218 + }), 219 + } 220 + } 221 + 222 + fn parse_record(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 223 + let start = self.expect(LexToken::Record)?; 224 + let name = self.parse_ident()?; 225 + self.expect(LexToken::LeftBrace)?; 226 + 227 + let mut fields = Vec::new(); 228 + let mut doc_comments = Vec::new(); 229 + 230 + while !matches!(self.current().token, LexToken::RightBrace) { 231 + if let LexToken::DocComment(comment) = &self.current().token { 232 + let span = self.current().span; 233 + doc_comments.push(DocComment { 234 + text: comment.clone(), 235 + span, 236 + }); 237 + self.advance(); 238 + } else { 239 + fields.push(self.parse_field(doc_comments.clone())?); 240 + doc_comments.clear(); 241 + } 242 + } 243 + 244 + let end = self.expect(LexToken::RightBrace)?; 245 + self.expect(LexToken::Semicolon)?; 246 + 247 + Ok(Item::Record(Record { 248 + docs: Vec::new(), 249 + annotations, 250 + name, 251 + fields, 252 + span: Span::new(start.start, end.end), 253 + })) 254 + } 255 + 256 + fn parse_field(&mut self, docs: Vec<DocComment>) -> Result<Field, ParseError> { 257 + let annotations = self.parse_annotations()?; 258 + let name = self.parse_ident()?; 259 + 260 + let optional = if matches!(self.current().token, LexToken::Question) { 261 + self.advance(); 262 + true 263 + } else { 264 + false 265 + }; 266 + 267 + self.expect(LexToken::Colon)?; 268 + let ty = self.parse_type()?; 269 + self.expect(LexToken::Comma)?; 270 + 271 + let span = Span::new(name.span.start, ty.span().end); 272 + 273 + Ok(Field { 274 + docs, 275 + annotations, 276 + name, 277 + ty, 278 + optional, 279 + span, 280 + }) 281 + } 282 + 283 + fn parse_alias(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 284 + let start = self.expect(LexToken::Alias)?; 285 + let name = self.parse_ident()?; 286 + self.expect(LexToken::Equals)?; 287 + let ty = self.parse_type()?; 288 + let end = self.expect(LexToken::Semicolon)?; 289 + 290 + Ok(Item::Alias(Alias { 291 + docs: Vec::new(), 292 + annotations, 293 + name, 294 + ty, 295 + span: Span::new(start.start, end.end), 296 + })) 297 + } 298 + 299 + fn parse_token(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 300 + let start = self.expect(LexToken::Token)?; 301 + let name = self.parse_ident()?; 302 + let end = self.expect(LexToken::Semicolon)?; 303 + 304 + Ok(Item::Token(Token { 305 + docs: Vec::new(), 306 + annotations, 307 + name, 308 + span: Span::new(start.start, end.end), 309 + })) 310 + } 311 + 312 + fn parse_query(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 313 + let start = self.expect(LexToken::Query)?; 314 + let name = self.parse_ident()?; 315 + self.expect(LexToken::LeftParen)?; 316 + 317 + let params = self.parse_params()?; 318 + 319 + self.expect(LexToken::RightParen)?; 320 + self.expect(LexToken::Colon)?; 321 + 322 + let output = self.parse_base_type()?; 323 + 324 + let returns = if matches!(self.current().token, LexToken::Pipe) { 325 + self.advance(); 326 + if matches!(self.current().token, LexToken::Error) { 327 + self.advance(); 328 + let errors = self.parse_errors()?; 329 + let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 330 + ReturnType::TypeWithErrors { 331 + success: output, 332 + errors, 333 + span: error_span, 334 + } 335 + } else { 336 + let mut types = alloc::vec![output]; 337 + types.push(self.parse_base_type()?); 338 + 339 + while matches!(self.current().token, LexToken::Pipe) { 340 + self.advance(); 341 + types.push(self.parse_base_type()?); 342 + } 343 + 344 + let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 345 + ReturnType::Type(Type::Union { types, span }) 346 + } 347 + } else { 348 + ReturnType::Type(output) 349 + }; 350 + 351 + let end = self.expect(LexToken::Semicolon)?; 352 + 353 + Ok(Item::Query(Query { 354 + docs: Vec::new(), 355 + annotations, 356 + name, 357 + params, 358 + returns, 359 + span: Span::new(start.start, end.end), 360 + })) 361 + } 362 + 363 + fn parse_procedure(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 364 + let start = self.expect(LexToken::Procedure)?; 365 + let name = self.parse_ident()?; 366 + self.expect(LexToken::LeftParen)?; 367 + 368 + let params = self.parse_params()?; 369 + 370 + self.expect(LexToken::RightParen)?; 371 + self.expect(LexToken::Colon)?; 372 + 373 + let output = self.parse_base_type()?; 374 + 375 + let returns = if matches!(self.current().token, LexToken::Pipe) { 376 + self.advance(); 377 + if matches!(self.current().token, LexToken::Error) { 378 + self.advance(); 379 + let errors = self.parse_errors()?; 380 + let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 381 + ReturnType::TypeWithErrors { 382 + success: output, 383 + errors, 384 + span: error_span, 385 + } 386 + } else { 387 + let mut types = alloc::vec![output]; 388 + types.push(self.parse_base_type()?); 389 + 390 + while matches!(self.current().token, LexToken::Pipe) { 391 + self.advance(); 392 + types.push(self.parse_base_type()?); 393 + } 394 + 395 + let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 396 + ReturnType::Type(Type::Union { types, span }) 397 + } 398 + } else { 399 + ReturnType::Type(output) 400 + }; 401 + 402 + let end = self.expect(LexToken::Semicolon)?; 403 + 404 + Ok(Item::Procedure(Procedure { 405 + docs: Vec::new(), 406 + annotations, 407 + name, 408 + params, 409 + returns, 410 + span: Span::new(start.start, end.end), 411 + })) 412 + } 413 + 414 + fn parse_subscription(&mut self, annotations: Vec<Annotation>) -> Result<Item, ParseError> { 415 + let start = self.expect(LexToken::Subscription)?; 416 + let name = self.parse_ident()?; 417 + self.expect(LexToken::LeftParen)?; 418 + 419 + let params = self.parse_params()?; 420 + 421 + self.expect(LexToken::RightParen)?; 422 + self.expect(LexToken::Colon)?; 423 + 424 + let messages = self.parse_type()?; 425 + 426 + let end = self.expect(LexToken::Semicolon)?; 427 + 428 + Ok(Item::Subscription(Subscription { 429 + docs: Vec::new(), 430 + annotations, 431 + name, 432 + params, 433 + messages, 434 + span: Span::new(start.start, end.end), 435 + })) 436 + } 437 + 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 + fn parse_use(&mut self) -> Result<Item, ParseError> { 458 + let start = self.expect(LexToken::Use)?; 459 + let path = self.parse_path()?; 460 + 461 + let imports = if matches!(self.current().token, LexToken::As) { 462 + self.advance(); 463 + let alias = self.parse_ident()?; 464 + UseImports::Items(alloc::vec![UseItem { 465 + name: path.segments.last().unwrap().clone(), 466 + alias: Some(alias), 467 + }]) 468 + } else { 469 + UseImports::All 470 + }; 471 + 472 + let end = self.expect(LexToken::Semicolon)?; 473 + 474 + Ok(Item::Use(Use { 475 + path, 476 + imports, 477 + span: Span::new(start.start, end.end), 478 + })) 479 + } 480 + 481 + fn parse_params(&mut self) -> Result<Vec<Field>, ParseError> { 482 + let mut params = Vec::new(); 483 + 484 + 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) { 489 + self.advance(); 490 + true 491 + } else { 492 + false 493 + }; 494 + 495 + self.expect(LexToken::Colon)?; 496 + let ty = self.parse_type()?; 497 + 498 + let span = Span::new(name.span.start, ty.span().end); 499 + 500 + params.push(Field { 501 + docs: Vec::new(), 502 + annotations, 503 + name, 504 + ty, 505 + optional, 506 + span, 507 + }); 508 + 509 + if matches!(self.current().token, LexToken::Comma) { 510 + self.advance(); 511 + } else { 512 + break; 513 + } 514 + } 515 + 516 + Ok(params) 517 + } 518 + 519 + fn parse_errors(&mut self) -> Result<Vec<ErrorDef>, ParseError> { 520 + self.expect(LexToken::LeftBrace)?; 521 + 522 + let mut errors = Vec::new(); 523 + let mut doc_comments = Vec::new(); 524 + 525 + while !matches!(self.current().token, LexToken::RightBrace) { 526 + if let LexToken::DocComment(comment) = &self.current().token { 527 + let span = self.current().span; 528 + doc_comments.push(DocComment { 529 + text: comment.clone(), 530 + span, 531 + }); 532 + self.advance(); 533 + } else { 534 + let name = self.parse_ident()?; 535 + let span = name.span; 536 + self.expect(LexToken::Comma)?; 537 + errors.push(ErrorDef { 538 + docs: doc_comments.clone(), 539 + name, 540 + span, 541 + }); 542 + doc_comments.clear(); 543 + } 544 + } 545 + 546 + self.expect(LexToken::RightBrace)?; 547 + 548 + Ok(errors) 549 + } 550 + 551 + fn parse_type(&mut self) -> Result<Type, ParseError> { 552 + let base = self.parse_base_type()?; 553 + 554 + if matches!(self.current().token, LexToken::Pipe) { 555 + let mut types = alloc::vec![base]; 556 + 557 + while matches!(self.current().token, LexToken::Pipe) { 558 + self.advance(); 559 + if matches!(self.current().token, LexToken::Error) { 560 + break; 561 + } 562 + types.push(self.parse_base_type()?); 563 + } 564 + 565 + let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 566 + return Ok(Type::Union { types, span }); 567 + } 568 + 569 + Ok(base) 570 + } 571 + 572 + fn parse_base_type(&mut self) -> Result<Type, ParseError> { 573 + let current = self.current(); 574 + let start = current.span.start; 575 + 576 + let mut ty = match &current.token { 577 + LexToken::String => { 578 + let span = self.advance().span; 579 + Type::Primitive { 580 + kind: PrimitiveType::String, 581 + span, 582 + } 583 + } 584 + LexToken::Integer => { 585 + let span = self.advance().span; 586 + Type::Primitive { 587 + kind: PrimitiveType::Integer, 588 + span, 589 + } 590 + } 591 + LexToken::Number => { 592 + let span = self.advance().span; 593 + Type::Primitive { 594 + kind: PrimitiveType::Number, 595 + span, 596 + } 597 + } 598 + LexToken::Boolean => { 599 + let span = self.advance().span; 600 + Type::Primitive { 601 + kind: PrimitiveType::Boolean, 602 + span, 603 + } 604 + } 605 + LexToken::Blob => { 606 + let span = self.advance().span; 607 + Type::Primitive { 608 + kind: PrimitiveType::Blob, 609 + span, 610 + } 611 + } 612 + LexToken::Bytes => { 613 + let span = self.advance().span; 614 + Type::Primitive { 615 + kind: PrimitiveType::Bytes, 616 + span, 617 + } 618 + } 619 + LexToken::Null => { 620 + let span = self.advance().span; 621 + Type::Primitive { 622 + kind: PrimitiveType::Null, 623 + span, 624 + } 625 + } 626 + LexToken::Unknown => { 627 + let span = self.advance().span; 628 + Type::Unknown { span } 629 + } 630 + LexToken::Ident(_) => { 631 + let path = self.parse_path()?; 632 + let span = path.span; 633 + Type::Reference { path, span } 634 + } 635 + LexToken::LeftBracket => { 636 + self.advance(); 637 + let inner = self.parse_type()?; 638 + let end = self.expect(LexToken::RightBracket)?; 639 + Type::Array { 640 + inner: alloc::boxed::Box::new(inner), 641 + span: Span::new(start, end.end), 642 + } 643 + } 644 + LexToken::LeftBrace => { 645 + self.advance(); 646 + let mut fields = Vec::new(); 647 + 648 + while !matches!(self.current().token, LexToken::RightBrace) { 649 + fields.push(self.parse_field(Vec::new())?); 650 + } 651 + 652 + let end = self.expect(LexToken::RightBrace)?; 653 + Type::Object { 654 + fields, 655 + span: Span::new(start, end.end), 656 + } 657 + } 658 + _ => { 659 + return Err(ParseError::Syntax { 660 + message: alloc::format!("Expected type, found {}", current.token), 661 + span: current.span, 662 + }); 663 + } 664 + }; 665 + 666 + // Handle array suffix: Type[] 667 + if matches!(self.current().token, LexToken::LeftBracket) { 668 + self.advance(); 669 + let end = self.expect(LexToken::RightBracket)?; 670 + ty = Type::Array { 671 + inner: alloc::boxed::Box::new(ty), 672 + span: Span::new(start, end.end), 673 + }; 674 + } 675 + 676 + if matches!(self.current().token, LexToken::Constrained) { 677 + self.advance(); 678 + self.expect(LexToken::LeftBrace)?; 679 + 680 + let mut constraints = Vec::new(); 681 + 682 + while !matches!(self.current().token, LexToken::RightBrace) { 683 + constraints.push(self.parse_constraint()?); 684 + 685 + if matches!(self.current().token, LexToken::Comma) { 686 + self.advance(); 687 + } else { 688 + break; 689 + } 690 + } 691 + 692 + let end = self.expect(LexToken::RightBrace)?; 693 + 694 + ty = Type::Constrained { 695 + base: alloc::boxed::Box::new(ty), 696 + constraints, 697 + span: Span::new(start, end.end), 698 + }; 699 + } 700 + 701 + Ok(ty) 702 + } 703 + 704 + fn parse_constraint(&mut self) -> Result<Constraint, ParseError> { 705 + let name = self.parse_ident()?; 706 + self.expect(LexToken::Colon)?; 707 + 708 + let start = name.span.start; 709 + let current = self.current(); 710 + let current_span = current.span; 711 + 712 + let constraint = match name.name.as_str() { 713 + "minLength" => { 714 + if let LexToken::IntLit(i) = current.token { 715 + let value = i; 716 + self.advance(); 717 + Constraint::MinLength { 718 + value: value as usize, 719 + span: Span::new(start, current_span.end), 720 + } 721 + } else { 722 + return Err(ParseError::Syntax { 723 + message: alloc::format!("Expected integer for minLength"), 724 + span: current_span, 725 + }); 726 + } 727 + } 728 + "maxLength" => { 729 + if let LexToken::IntLit(i) = current.token { 730 + let value = i; 731 + self.advance(); 732 + Constraint::MaxLength { 733 + value: value as usize, 734 + span: Span::new(start, current_span.end), 735 + } 736 + } else { 737 + return Err(ParseError::Syntax { 738 + message: alloc::format!("Expected integer for maxLength"), 739 + span: current_span, 740 + }); 741 + } 742 + } 743 + "minimum" => { 744 + if let LexToken::IntLit(i) = current.token { 745 + let value = i; 746 + self.advance(); 747 + Constraint::Minimum { 748 + value, 749 + span: Span::new(start, current_span.end), 750 + } 751 + } else { 752 + return Err(ParseError::Syntax { 753 + message: alloc::format!("Expected integer for minimum"), 754 + span: current_span, 755 + }); 756 + } 757 + } 758 + "maximum" => { 759 + if let LexToken::IntLit(i) = current.token { 760 + let value = i; 761 + self.advance(); 762 + Constraint::Maximum { 763 + value, 764 + span: Span::new(start, current_span.end), 765 + } 766 + } else { 767 + return Err(ParseError::Syntax { 768 + message: alloc::format!("Expected integer for maximum"), 769 + span: current_span, 770 + }); 771 + } 772 + } 773 + "enum" => { 774 + if matches!(current.token, LexToken::LeftBracket) { 775 + self.advance(); 776 + let mut values = Vec::new(); 777 + 778 + while !matches!(self.current().token, LexToken::RightBracket) { 779 + let current = self.current(); 780 + match &current.token { 781 + LexToken::StringLit(s) => { 782 + values.push(s.clone()); 783 + self.advance(); 784 + } 785 + _ => { 786 + return Err(ParseError::Syntax { 787 + message: alloc::format!("Expected string literal in enum"), 788 + span: current.span, 789 + }); 790 + } 791 + } 792 + 793 + if matches!(self.current().token, LexToken::Comma) { 794 + self.advance(); 795 + } else { 796 + break; 797 + } 798 + } 799 + 800 + let end = self.expect(LexToken::RightBracket)?; 801 + Constraint::Enum { 802 + values, 803 + span: Span::new(start, end.end), 804 + } 805 + } else { 806 + return Err(ParseError::Syntax { 807 + message: alloc::format!("Expected array for enum"), 808 + span: current_span, 809 + }); 810 + } 811 + } 812 + "format" => { 813 + if let LexToken::StringLit(s) = &current.token { 814 + let value = s.clone(); 815 + self.advance(); 816 + Constraint::Format { 817 + value, 818 + span: Span::new(start, current_span.end), 819 + } 820 + } else { 821 + return Err(ParseError::Syntax { 822 + message: alloc::format!("Expected string for format"), 823 + span: current_span, 824 + }); 825 + } 826 + } 827 + "minGraphemes" => { 828 + if let LexToken::IntLit(i) = current.token { 829 + let value = i; 830 + self.advance(); 831 + Constraint::MinGraphemes { 832 + value: value as usize, 833 + span: Span::new(start, current_span.end), 834 + } 835 + } else { 836 + return Err(ParseError::Syntax { 837 + message: alloc::format!("Expected integer for minGraphemes"), 838 + span: current_span, 839 + }); 840 + } 841 + } 842 + "maxGraphemes" => { 843 + if let LexToken::IntLit(i) = current.token { 844 + let value = i; 845 + self.advance(); 846 + Constraint::MaxGraphemes { 847 + value: value as usize, 848 + span: Span::new(start, current_span.end), 849 + } 850 + } else { 851 + return Err(ParseError::Syntax { 852 + message: alloc::format!("Expected integer for maxGraphemes"), 853 + span: current_span, 854 + }); 855 + } 856 + } 857 + "maxSize" => { 858 + if let LexToken::IntLit(i) = current.token { 859 + let value = i; 860 + self.advance(); 861 + Constraint::MaxSize { 862 + value: value as usize, 863 + span: Span::new(start, current_span.end), 864 + } 865 + } else { 866 + return Err(ParseError::Syntax { 867 + message: alloc::format!("Expected integer for maxSize"), 868 + span: current_span, 869 + }); 870 + } 871 + } 872 + "accept" => { 873 + if matches!(current.token, LexToken::LeftBracket) { 874 + self.advance(); 875 + let mut mimes = Vec::new(); 876 + 877 + while !matches!(self.current().token, LexToken::RightBracket) { 878 + let current = self.current(); 879 + match &current.token { 880 + LexToken::StringLit(s) => { 881 + mimes.push(s.clone()); 882 + self.advance(); 883 + } 884 + _ => { 885 + return Err(ParseError::Syntax { 886 + message: alloc::format!("Expected string literal in accept"), 887 + span: current.span, 888 + }); 889 + } 890 + } 891 + 892 + if matches!(self.current().token, LexToken::Comma) { 893 + self.advance(); 894 + } else { 895 + break; 896 + } 897 + } 898 + 899 + let end = self.expect(LexToken::RightBracket)?; 900 + Constraint::Accept { 901 + mimes, 902 + span: Span::new(start, end.end), 903 + } 904 + } else { 905 + return Err(ParseError::Syntax { 906 + message: alloc::format!("Expected array for accept"), 907 + span: current_span, 908 + }); 909 + } 910 + } 911 + "knownValues" => { 912 + if matches!(current.token, LexToken::LeftBracket) { 913 + self.advance(); 914 + let mut values = Vec::new(); 915 + 916 + while !matches!(self.current().token, LexToken::RightBracket) { 917 + values.push(self.parse_path()?); 918 + 919 + if matches!(self.current().token, LexToken::Comma) { 920 + self.advance(); 921 + } else { 922 + break; 923 + } 924 + } 925 + 926 + let end = self.expect(LexToken::RightBracket)?; 927 + Constraint::KnownValues { 928 + values, 929 + span: Span::new(start, end.end), 930 + } 931 + } else { 932 + return Err(ParseError::Syntax { 933 + message: alloc::format!("Expected array for knownValues"), 934 + span: current_span, 935 + }); 936 + } 937 + } 938 + "default" => { 939 + use crate::ast::ConstraintValue; 940 + let end_span = current_span.end; 941 + let value = match &current.token { 942 + LexToken::StringLit(s) => { 943 + let v = ConstraintValue::String(s.clone()); 944 + self.advance(); 945 + v 946 + } 947 + LexToken::IntLit(i) => { 948 + let v = ConstraintValue::Integer(*i); 949 + self.advance(); 950 + v 951 + } 952 + LexToken::True => { 953 + let v = ConstraintValue::Boolean(true); 954 + self.advance(); 955 + v 956 + } 957 + LexToken::False => { 958 + let v = ConstraintValue::Boolean(false); 959 + self.advance(); 960 + v 961 + } 962 + _ => { 963 + return Err(ParseError::Syntax { 964 + message: alloc::format!("Expected string, integer, or boolean for default"), 965 + span: current_span, 966 + }); 967 + } 968 + }; 969 + Constraint::Default { 970 + value, 971 + span: Span::new(start, end_span), 972 + } 973 + } 974 + _ => { 975 + return Err(ParseError::Syntax { 976 + message: alloc::format!("Unknown constraint: {}", name.name), 977 + span: name.span, 978 + }); 979 + } 980 + }; 981 + 982 + Ok(constraint) 983 + } 984 + } 985 + 986 + #[cfg(test)] 987 + mod tests { 988 + use super::*; 989 + 990 + #[test] 991 + fn test_parse_record() { 992 + let input = r#"record user { 993 + name: string, 994 + age: integer, 995 + };"#; 996 + let result = parse_lexicon(input); 997 + assert!(result.is_ok()); 998 + let lexicon = result.unwrap(); 999 + assert_eq!(lexicon.items.len(), 1); 1000 + match &lexicon.items[0] { 1001 + Item::Record(r) => { 1002 + assert_eq!(r.name.name, "user"); 1003 + assert_eq!(r.fields.len(), 2); 1004 + } 1005 + _ => panic!("Expected record"), 1006 + } 1007 + } 1008 + 1009 + #[test] 1010 + fn test_parse_alias() { 1011 + let input = "alias userId = string;"; 1012 + let result = parse_lexicon(input); 1013 + assert!(result.is_ok()); 1014 + let lexicon = result.unwrap(); 1015 + assert_eq!(lexicon.items.len(), 1); 1016 + match &lexicon.items[0] { 1017 + Item::Alias(a) => { 1018 + assert_eq!(a.name.name, "userId"); 1019 + } 1020 + _ => panic!("Expected alias"), 1021 + } 1022 + } 1023 + 1024 + #[test] 1025 + fn test_parse_token() { 1026 + let input = "token like;"; 1027 + let result = parse_lexicon(input); 1028 + assert!(result.is_ok()); 1029 + let lexicon = result.unwrap(); 1030 + assert_eq!(lexicon.items.len(), 1); 1031 + match &lexicon.items[0] { 1032 + Item::Token(t) => { 1033 + assert_eq!(t.name.name, "like"); 1034 + } 1035 + _ => panic!("Expected token"), 1036 + } 1037 + } 1038 + 1039 + #[test] 1040 + fn test_parse_query() { 1041 + let input = "query getUser(id: string,): user;"; 1042 + let result = parse_lexicon(input); 1043 + assert!(result.is_ok()); 1044 + let lexicon = result.unwrap(); 1045 + assert_eq!(lexicon.items.len(), 1); 1046 + match &lexicon.items[0] { 1047 + Item::Query(q) => { 1048 + assert_eq!(q.name.name, "getUser"); 1049 + assert_eq!(q.params.len(), 1); 1050 + } 1051 + _ => panic!("Expected query"), 1052 + } 1053 + } 1054 + 1055 + #[test] 1056 + fn test_parse_query_with_errors() { 1057 + let input = r#"query getUser(id: string,): user | error { 1058 + /// User not found 1059 + NotFound, 1060 + };"#; 1061 + let result = parse_lexicon(input); 1062 + if let Err(e) = &result { 1063 + eprintln!("Parse error: {:?}", e); 1064 + } 1065 + assert!(result.is_ok()); 1066 + let lexicon = result.unwrap(); 1067 + assert_eq!(lexicon.items.len(), 1); 1068 + match &lexicon.items[0] { 1069 + Item::Query(q) => { 1070 + assert_eq!(q.name.name, "getUser"); 1071 + match &q.returns { 1072 + ReturnType::TypeWithErrors { errors, .. } => { 1073 + assert_eq!(errors.len(), 1); 1074 + assert_eq!(errors[0].name.name, "NotFound"); 1075 + } 1076 + _ => panic!("Expected TypeWithErrors"), 1077 + } 1078 + } 1079 + _ => panic!("Expected query"), 1080 + } 1081 + } 1082 + 1083 + #[test] 1084 + fn test_parse_subscription() { 1085 + let input = "subscription subscribeRepos(cursor?: integer,): commit | identity;"; 1086 + let result = parse_lexicon(input); 1087 + assert!(result.is_ok()); 1088 + let lexicon = result.unwrap(); 1089 + assert_eq!(lexicon.items.len(), 1); 1090 + match &lexicon.items[0] { 1091 + Item::Subscription(s) => { 1092 + assert_eq!(s.name.name, "subscribeRepos"); 1093 + assert_eq!(s.params.len(), 1); 1094 + assert!(s.params[0].optional); 1095 + } 1096 + _ => panic!("Expected subscription"), 1097 + } 1098 + } 1099 + 1100 + #[test] 1101 + fn test_parse_namespace() { 1102 + let input = "namespace actor;"; 1103 + let result = parse_lexicon(input); 1104 + assert!(result.is_ok()); 1105 + let lexicon = result.unwrap(); 1106 + assert_eq!(lexicon.items.len(), 1); 1107 + match &lexicon.items[0] { 1108 + Item::Namespace(n) => { 1109 + assert_eq!(n.name.name, "actor"); 1110 + } 1111 + _ => panic!("Expected namespace"), 1112 + } 1113 + } 1114 + 1115 + #[test] 1116 + fn test_parse_constrained_type() { 1117 + let input = r#"alias shortString = string constrained { 1118 + maxLength: 100, 1119 + };"#; 1120 + let result = parse_lexicon(input); 1121 + assert!(result.is_ok()); 1122 + let lexicon = result.unwrap(); 1123 + assert_eq!(lexicon.items.len(), 1); 1124 + match &lexicon.items[0] { 1125 + Item::Alias(a) => { 1126 + match &a.ty { 1127 + Type::Constrained { constraints, .. } => { 1128 + assert_eq!(constraints.len(), 1); 1129 + } 1130 + _ => panic!("Expected constrained type"), 1131 + } 1132 + } 1133 + _ => panic!("Expected alias"), 1134 + } 1135 + } 1136 + 1137 + #[test] 1138 + fn test_parse_union_type() { 1139 + let input = "alias result = success | failure;"; 1140 + let result = parse_lexicon(input); 1141 + assert!(result.is_ok()); 1142 + let lexicon = result.unwrap(); 1143 + assert_eq!(lexicon.items.len(), 1); 1144 + match &lexicon.items[0] { 1145 + Item::Alias(a) => { 1146 + match &a.ty { 1147 + Type::Union { types, .. } => { 1148 + assert_eq!(types.len(), 2); 1149 + } 1150 + _ => panic!("Expected union type"), 1151 + } 1152 + } 1153 + _ => panic!("Expected alias"), 1154 + } 1155 + } 1156 + 1157 + #[test] 1158 + fn test_parse_array_type() { 1159 + let input = "alias userList = [user];"; 1160 + let result = parse_lexicon(input); 1161 + assert!(result.is_ok()); 1162 + let lexicon = result.unwrap(); 1163 + assert_eq!(lexicon.items.len(), 1); 1164 + match &lexicon.items[0] { 1165 + Item::Alias(a) => { 1166 + match &a.ty { 1167 + Type::Array { .. } => {} 1168 + _ => panic!("Expected array type"), 1169 + } 1170 + } 1171 + _ => panic!("Expected alias"), 1172 + } 1173 + } 1174 + 1175 + #[test] 1176 + fn test_parse_annotation() { 1177 + let input = "@deprecated\nrecord old {};"; 1178 + let result = parse_lexicon(input); 1179 + assert!(result.is_ok()); 1180 + let lexicon = result.unwrap(); 1181 + assert_eq!(lexicon.items.len(), 1); 1182 + match &lexicon.items[0] { 1183 + Item::Record(r) => { 1184 + assert_eq!(r.annotations.len(), 1); 1185 + assert_eq!(r.annotations[0].name.name, "deprecated"); 1186 + } 1187 + _ => panic!("Expected record"), 1188 + } 1189 + } 1190 + 1191 + #[test] 1192 + fn test_parse_annotation_with_args() { 1193 + let input = "@validate(min: 0, max: 100)\nrecord data {};"; 1194 + let result = parse_lexicon(input); 1195 + assert!(result.is_ok()); 1196 + let lexicon = result.unwrap(); 1197 + assert_eq!(lexicon.items.len(), 1); 1198 + match &lexicon.items[0] { 1199 + Item::Record(r) => { 1200 + assert_eq!(r.annotations.len(), 1); 1201 + assert_eq!(r.annotations[0].args.len(), 2); 1202 + } 1203 + _ => panic!("Expected record"), 1204 + } 1205 + } 1206 + 1207 + #[test] 1208 + fn test_parse_optional_field() { 1209 + let input = r#"record user { 1210 + name?: string, 1211 + };"#; 1212 + let result = parse_lexicon(input); 1213 + assert!(result.is_ok()); 1214 + let lexicon = result.unwrap(); 1215 + match &lexicon.items[0] { 1216 + Item::Record(r) => { 1217 + assert_eq!(r.fields.len(), 1); 1218 + assert!(r.fields[0].optional); 1219 + } 1220 + _ => panic!("Expected record"), 1221 + } 1222 + } 1223 + 1224 + #[test] 1225 + fn test_parse_enum_constraint() { 1226 + let input = r#"alias status = string constrained { 1227 + enum: ["active", "inactive"], 1228 + };"#; 1229 + let result = parse_lexicon(input); 1230 + assert!(result.is_ok()); 1231 + let lexicon = result.unwrap(); 1232 + match &lexicon.items[0] { 1233 + Item::Alias(a) => { 1234 + match &a.ty { 1235 + Type::Constrained { constraints, .. } => { 1236 + match &constraints[0] { 1237 + Constraint::Enum { values, .. } => { 1238 + assert_eq!(values.len(), 2); 1239 + } 1240 + _ => panic!("Expected enum constraint"), 1241 + } 1242 + } 1243 + _ => panic!("Expected constrained type"), 1244 + } 1245 + } 1246 + _ => panic!("Expected alias"), 1247 + } 1248 + } 1249 + }
+30
mlf-lang/src/span.rs
··· 1 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 2 + pub struct Span { 3 + pub start: usize, 4 + pub end: usize, 5 + } 6 + 7 + impl Span { 8 + pub fn new(start: usize, end: usize) -> Self { 9 + Self { start, end } 10 + } 11 + 12 + pub fn len(&self) -> usize { 13 + self.end - self.start 14 + } 15 + 16 + pub fn is_empty(&self) -> bool { 17 + self.start == self.end 18 + } 19 + 20 + pub fn merge(&self, other: &Span) -> Span { 21 + Span { 22 + start: self.start.min(other.start), 23 + end: self.end.max(other.end), 24 + } 25 + } 26 + } 27 + 28 + pub trait Spanned { 29 + fn span(&self) -> Span; 30 + }
+5
mlf-lang/src/validate.rs
··· 1 + use crate::{Lexicon, error::ValidationErrors}; 2 + 3 + pub fn validate_lexicon(_lexicon: &Lexicon) -> ValidationErrors { 4 + ValidationErrors::new() 5 + }
+1363
mlf-lang/src/workspace.rs
··· 1 + use alloc::string::String; 2 + use alloc::vec::Vec; 3 + use alloc::collections::BTreeMap; 4 + use crate::{ast::*, error::{ValidationError, ValidationErrors}, span::Span}; 5 + 6 + #[derive(Debug, Clone, PartialEq)] 7 + pub struct Workspace { 8 + modules: BTreeMap<String, Module>, 9 + } 10 + 11 + #[derive(Debug, Clone, PartialEq)] 12 + struct Module { 13 + namespace: String, 14 + lexicon: Lexicon, 15 + symbols: SymbolTable, 16 + imports: ImportTable, 17 + } 18 + 19 + #[derive(Debug, Clone, PartialEq)] 20 + struct SymbolTable { 21 + types: BTreeMap<String, Symbol>, 22 + } 23 + 24 + #[derive(Debug, Clone, PartialEq, Default)] 25 + struct ImportTable { 26 + mappings: BTreeMap<String, ImportedSymbol>, 27 + } 28 + 29 + #[derive(Debug, Clone, PartialEq)] 30 + struct ImportedSymbol { 31 + original_path: Vec<String>, 32 + local_name: String, 33 + } 34 + 35 + #[derive(Debug, Clone, PartialEq)] 36 + enum Symbol { 37 + Record { name: String, span: Span }, 38 + Alias { name: String, span: Span }, 39 + Token { name: String, span: Span }, 40 + } 41 + 42 + impl Workspace { 43 + pub fn new() -> Self { 44 + Self { 45 + modules: BTreeMap::new(), 46 + } 47 + } 48 + 49 + pub fn with_prelude() -> Result<Self, ValidationErrors> { 50 + let mut ws = Self::new(); 51 + let prelude_lexicon = crate::parser::parse_lexicon(crate::PRELUDE) 52 + .map_err(|e| { 53 + let mut errors = ValidationErrors::new(); 54 + errors.push(ValidationError::InvalidConstraint { 55 + message: alloc::format!("Failed to parse prelude: {:?}", e), 56 + span: crate::span::Span::new(0, 0), 57 + }); 58 + errors 59 + })?; 60 + ws.add_module("prelude".into(), prelude_lexicon)?; 61 + Ok(ws) 62 + } 63 + 64 + pub fn add_module(&mut self, namespace: String, lexicon: Lexicon) -> Result<(), ValidationErrors> { 65 + let symbols = Self::build_symbol_table(&namespace, &lexicon)?; 66 + 67 + let module = Module { 68 + namespace: namespace.clone(), 69 + lexicon, 70 + symbols, 71 + imports: ImportTable::default(), 72 + }; 73 + 74 + self.modules.insert(namespace, module); 75 + Ok(()) 76 + } 77 + 78 + pub fn resolve(&mut self) -> Result<(), ValidationErrors> { 79 + let mut errors = ValidationErrors::new(); 80 + 81 + if let Err(mut import_errors) = self.resolve_imports() { 82 + errors.append(&mut import_errors); 83 + } 84 + 85 + let modules: Vec<(String, Module)> = self.modules.iter() 86 + .map(|(k, v)| (k.clone(), v.clone())) 87 + .collect(); 88 + 89 + for (namespace, module) in modules { 90 + if let Err(mut module_errors) = self.resolve_module(&namespace, &module) { 91 + errors.append(&mut module_errors); 92 + } 93 + } 94 + 95 + if !errors.is_empty() { 96 + return Err(errors); 97 + } 98 + 99 + if let Err(mut typecheck_errors) = self.typecheck() { 100 + errors.append(&mut typecheck_errors); 101 + } 102 + 103 + if errors.is_empty() { 104 + Ok(()) 105 + } else { 106 + Err(errors) 107 + } 108 + } 109 + 110 + fn typecheck(&self) -> Result<(), ValidationErrors> { 111 + let mut errors = ValidationErrors::new(); 112 + 113 + for (namespace, module) in &self.modules { 114 + if let Err(mut module_errors) = self.typecheck_module(namespace, module) { 115 + errors.append(&mut module_errors); 116 + } 117 + } 118 + 119 + if errors.is_empty() { 120 + Ok(()) 121 + } else { 122 + Err(errors) 123 + } 124 + } 125 + 126 + fn typecheck_module(&self, namespace: &str, module: &Module) -> Result<(), ValidationErrors> { 127 + let mut errors = ValidationErrors::new(); 128 + 129 + for item in &module.lexicon.items { 130 + if let Err(mut item_errors) = self.typecheck_item(namespace, item) { 131 + errors.append(&mut item_errors); 132 + } 133 + } 134 + 135 + if errors.is_empty() { 136 + Ok(()) 137 + } else { 138 + Err(errors) 139 + } 140 + } 141 + 142 + fn typecheck_item(&self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> { 143 + match item { 144 + Item::Alias(a) => self.typecheck_alias(namespace, a), 145 + Item::Record(r) => self.typecheck_record(namespace, r), 146 + _ => Ok(()), 147 + } 148 + } 149 + 150 + fn typecheck_alias(&self, namespace: &str, alias: &Alias) -> Result<(), ValidationErrors> { 151 + self.typecheck_type(namespace, &alias.ty) 152 + } 153 + 154 + fn typecheck_record(&self, namespace: &str, record: &Record) -> Result<(), ValidationErrors> { 155 + let mut errors = ValidationErrors::new(); 156 + 157 + for field in &record.fields { 158 + if let Err(mut field_errors) = self.typecheck_type(namespace, &field.ty) { 159 + errors.append(&mut field_errors); 160 + } 161 + } 162 + 163 + if errors.is_empty() { 164 + Ok(()) 165 + } else { 166 + Err(errors) 167 + } 168 + } 169 + 170 + fn typecheck_type(&self, namespace: &str, ty: &Type) -> Result<(), ValidationErrors> { 171 + match ty { 172 + Type::Primitive { .. } | Type::Unknown { .. } => Ok(()), 173 + Type::Reference { .. } => Ok(()), 174 + Type::Array { inner, .. } => self.typecheck_type(namespace, inner), 175 + Type::Union { types, span } => { 176 + let mut errors = ValidationErrors::new(); 177 + 178 + for ty in types { 179 + if let Err(mut ty_errors) = self.typecheck_type(namespace, ty) { 180 + errors.append(&mut ty_errors); 181 + } 182 + } 183 + 184 + if types.is_empty() { 185 + errors.push(ValidationError::InvalidConstraint { 186 + message: "Union type must have at least one member".into(), 187 + span: *span, 188 + }); 189 + } 190 + 191 + if errors.is_empty() { 192 + Ok(()) 193 + } else { 194 + Err(errors) 195 + } 196 + } 197 + Type::Object { fields, .. } => { 198 + let mut errors = ValidationErrors::new(); 199 + 200 + for field in fields { 201 + if let Err(mut field_errors) = self.typecheck_type(namespace, &field.ty) { 202 + errors.append(&mut field_errors); 203 + } 204 + } 205 + 206 + if errors.is_empty() { 207 + Ok(()) 208 + } else { 209 + Err(errors) 210 + } 211 + } 212 + Type::Constrained { base, constraints, span } => { 213 + let mut errors = ValidationErrors::new(); 214 + 215 + if let Err(mut base_errors) = self.typecheck_type(namespace, base) { 216 + errors.append(&mut base_errors); 217 + } 218 + 219 + if let Err(mut constraint_errors) = self.typecheck_constraints(base, constraints, *span) { 220 + errors.append(&mut constraint_errors); 221 + } 222 + 223 + if let Err(mut refinement_errors) = self.check_constraint_refinement(base, constraints) { 224 + errors.append(&mut refinement_errors); 225 + } 226 + 227 + if errors.is_empty() { 228 + Ok(()) 229 + } else { 230 + Err(errors) 231 + } 232 + } 233 + } 234 + } 235 + 236 + fn typecheck_constraints(&self, base: &Type, constraints: &[Constraint], _span: Span) -> Result<(), ValidationErrors> { 237 + let mut errors = ValidationErrors::new(); 238 + 239 + let base_kind = self.get_base_primitive(base); 240 + 241 + for constraint in constraints { 242 + match constraint { 243 + Constraint::MinLength { span, .. } 244 + | Constraint::MaxLength { span, .. } 245 + | Constraint::MinGraphemes { span, .. } 246 + | Constraint::MaxGraphemes { span, .. } 247 + | Constraint::Format { span, .. } 248 + | Constraint::Enum { span, .. } => { 249 + if !matches!(base_kind, Some(PrimitiveType::String)) { 250 + errors.push(ValidationError::InvalidConstraint { 251 + message: alloc::format!("String constraint on non-string type"), 252 + span: *span, 253 + }); 254 + } 255 + } 256 + Constraint::Minimum { span, .. } 257 + | Constraint::Maximum { span, .. } => { 258 + if !matches!(base_kind, Some(PrimitiveType::Integer) | Some(PrimitiveType::Number)) { 259 + errors.push(ValidationError::InvalidConstraint { 260 + message: alloc::format!("Numeric constraint on non-numeric type"), 261 + span: *span, 262 + }); 263 + } 264 + } 265 + Constraint::Accept { span, .. } 266 + | Constraint::MaxSize { span, .. } => { 267 + if !matches!(base_kind, Some(PrimitiveType::Blob)) { 268 + errors.push(ValidationError::InvalidConstraint { 269 + message: alloc::format!("Blob constraint on non-blob type"), 270 + span: *span, 271 + }); 272 + } 273 + } 274 + Constraint::KnownValues { .. } => {} 275 + Constraint::Default { .. } => {} 276 + } 277 + } 278 + 279 + if errors.is_empty() { 280 + Ok(()) 281 + } else { 282 + Err(errors) 283 + } 284 + } 285 + 286 + fn check_constraint_refinement(&self, base: &Type, new_constraints: &[Constraint]) -> Result<(), ValidationErrors> { 287 + let base_constraints = self.get_base_constraints(base); 288 + 289 + if base_constraints.is_empty() { 290 + return Ok(()); 291 + } 292 + 293 + let mut errors = ValidationErrors::new(); 294 + 295 + for new_constraint in new_constraints { 296 + match new_constraint { 297 + Constraint::MaxLength { value: new_max, span } => { 298 + for base_constraint in &base_constraints { 299 + if let Constraint::MaxLength { value: base_max, .. } = base_constraint { 300 + if new_max > base_max { 301 + errors.push(ValidationError::ConstraintTooPermissive { 302 + message: alloc::format!( 303 + "maxLength {} is greater than base maxLength {}", 304 + new_max, base_max 305 + ), 306 + span: *span, 307 + }); 308 + } 309 + } 310 + } 311 + } 312 + Constraint::MinLength { value: new_min, span } => { 313 + for base_constraint in &base_constraints { 314 + if let Constraint::MinLength { value: base_min, .. } = base_constraint { 315 + if new_min < base_min { 316 + errors.push(ValidationError::ConstraintTooPermissive { 317 + message: alloc::format!( 318 + "minLength {} is less than base minLength {}", 319 + new_min, base_min 320 + ), 321 + span: *span, 322 + }); 323 + } 324 + } 325 + } 326 + } 327 + Constraint::Maximum { value: new_max, span } => { 328 + for base_constraint in &base_constraints { 329 + if let Constraint::Maximum { value: base_max, .. } = base_constraint { 330 + if new_max > base_max { 331 + errors.push(ValidationError::ConstraintTooPermissive { 332 + message: alloc::format!( 333 + "maximum {} is greater than base maximum {}", 334 + new_max, base_max 335 + ), 336 + span: *span, 337 + }); 338 + } 339 + } 340 + } 341 + } 342 + Constraint::Minimum { value: new_min, span } => { 343 + for base_constraint in &base_constraints { 344 + if let Constraint::Minimum { value: base_min, .. } = base_constraint { 345 + if new_min < base_min { 346 + errors.push(ValidationError::ConstraintTooPermissive { 347 + message: alloc::format!( 348 + "minimum {} is less than base minimum {}", 349 + new_min, base_min 350 + ), 351 + span: *span, 352 + }); 353 + } 354 + } 355 + } 356 + } 357 + Constraint::MaxGraphemes { value: new_max, span } => { 358 + for base_constraint in &base_constraints { 359 + if let Constraint::MaxGraphemes { value: base_max, .. } = base_constraint { 360 + if new_max > base_max { 361 + errors.push(ValidationError::ConstraintTooPermissive { 362 + message: alloc::format!( 363 + "maxGraphemes {} is greater than base maxGraphemes {}", 364 + new_max, base_max 365 + ), 366 + span: *span, 367 + }); 368 + } 369 + } 370 + } 371 + } 372 + Constraint::MinGraphemes { value: new_min, span } => { 373 + for base_constraint in &base_constraints { 374 + if let Constraint::MinGraphemes { value: base_min, .. } = base_constraint { 375 + if new_min < base_min { 376 + errors.push(ValidationError::ConstraintTooPermissive { 377 + message: alloc::format!( 378 + "minGraphemes {} is less than base minGraphemes {}", 379 + new_min, base_min 380 + ), 381 + span: *span, 382 + }); 383 + } 384 + } 385 + } 386 + } 387 + Constraint::MaxSize { value: new_max, span } => { 388 + for base_constraint in &base_constraints { 389 + if let Constraint::MaxSize { value: base_max, .. } = base_constraint { 390 + if new_max > base_max { 391 + errors.push(ValidationError::ConstraintTooPermissive { 392 + message: alloc::format!( 393 + "maxSize {} is greater than base maxSize {}", 394 + new_max, base_max 395 + ), 396 + span: *span, 397 + }); 398 + } 399 + } 400 + } 401 + } 402 + Constraint::Enum { values: new_values, span } => { 403 + for base_constraint in &base_constraints { 404 + if let Constraint::Enum { values: base_values, .. } = base_constraint { 405 + for new_val in new_values { 406 + if !base_values.contains(new_val) { 407 + errors.push(ValidationError::ConstraintTooPermissive { 408 + message: alloc::format!( 409 + "enum value '{}' not in base enum", 410 + new_val 411 + ), 412 + span: *span, 413 + }); 414 + } 415 + } 416 + } 417 + } 418 + } 419 + _ => {} 420 + } 421 + } 422 + 423 + if errors.is_empty() { 424 + Ok(()) 425 + } else { 426 + Err(errors) 427 + } 428 + } 429 + 430 + fn get_base_constraints(&self, ty: &Type) -> Vec<Constraint> { 431 + match ty { 432 + Type::Constrained { base, constraints, .. } => { 433 + let mut all_constraints = constraints.clone(); 434 + all_constraints.extend(self.get_base_constraints(base)); 435 + all_constraints 436 + } 437 + Type::Reference { path, .. } => { 438 + if let Some(resolved_ty) = self.resolve_type_reference(path) { 439 + self.get_base_constraints(&resolved_ty) 440 + } else { 441 + Vec::new() 442 + } 443 + } 444 + _ => Vec::new(), 445 + } 446 + } 447 + 448 + fn get_base_primitive(&self, ty: &Type) -> Option<PrimitiveType> { 449 + match ty { 450 + Type::Primitive { kind, .. } => Some(*kind), 451 + Type::Constrained { base, .. } => self.get_base_primitive(base), 452 + Type::Reference { path, .. } => { 453 + if let Some(resolved_ty) = self.resolve_type_reference(path) { 454 + self.get_base_primitive(&resolved_ty) 455 + } else { 456 + None 457 + } 458 + } 459 + _ => None, 460 + } 461 + } 462 + 463 + fn resolve_type_reference(&self, path: &Path) -> Option<Type> { 464 + if path.segments.len() == 1 { 465 + let name = &path.segments[0].name; 466 + 467 + for (_, module) in &self.modules { 468 + if let Some(Symbol::Alias { .. }) = module.symbols.types.get(name) { 469 + 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()); 473 + } 474 + } 475 + } 476 + } 477 + } 478 + } else { 479 + let target_namespace = path.segments[..path.segments.len() - 1] 480 + .iter() 481 + .map(|s| s.name.as_str()) 482 + .collect::<Vec<_>>() 483 + .join("."); 484 + let type_name = &path.segments[path.segments.len() - 1].name; 485 + 486 + if let Some(module) = self.modules.get(&target_namespace) { 487 + 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()); 491 + } 492 + } 493 + } 494 + } 495 + } 496 + 497 + None 498 + } 499 + 500 + fn resolve_imports(&mut self) -> Result<(), ValidationErrors> { 501 + let mut errors = ValidationErrors::new(); 502 + 503 + let namespaces: Vec<String> = self.modules.keys().cloned().collect(); 504 + 505 + for namespace in namespaces { 506 + let lexicon = self.modules[&namespace].lexicon.clone(); 507 + 508 + for item in &lexicon.items { 509 + if let Item::Use(use_stmt) = item { 510 + if let Err(mut use_errors) = self.resolve_use_statement(&namespace, use_stmt) { 511 + errors.append(&mut use_errors); 512 + } 513 + } 514 + } 515 + } 516 + 517 + if errors.is_empty() { 518 + Ok(()) 519 + } else { 520 + Err(errors) 521 + } 522 + } 523 + 524 + fn resolve_use_statement(&mut self, current_namespace: &str, use_stmt: &Use) -> Result<(), ValidationErrors> { 525 + let mut errors = ValidationErrors::new(); 526 + 527 + let (target_namespace, type_name_opt) = match &use_stmt.imports { 528 + UseImports::All => { 529 + (use_stmt.path.to_string(), None) 530 + } 531 + UseImports::Items(_) => { 532 + if use_stmt.path.segments.len() < 2 { 533 + errors.push(ValidationError::UndefinedReference { 534 + name: use_stmt.path.to_string(), 535 + span: use_stmt.path.span, 536 + }); 537 + return Err(errors); 538 + } 539 + let namespace = use_stmt.path.segments[..use_stmt.path.segments.len() - 1] 540 + .iter() 541 + .map(|s| s.name.as_str()) 542 + .collect::<Vec<_>>() 543 + .join("."); 544 + let type_name = use_stmt.path.segments.last().unwrap().name.clone(); 545 + (namespace, Some(type_name)) 546 + } 547 + }; 548 + 549 + if !self.modules.contains_key(&target_namespace) { 550 + errors.push(ValidationError::UndefinedReference { 551 + name: target_namespace.clone(), 552 + span: use_stmt.path.span, 553 + }); 554 + return Err(errors); 555 + } 556 + 557 + let imports_to_add: Vec<(String, ImportedSymbol)> = match &use_stmt.imports { 558 + UseImports::All => { 559 + let target_module = self.modules.get(&target_namespace).unwrap(); 560 + target_module.symbols.types.keys() 561 + .map(|type_name| { 562 + let imported = ImportedSymbol { 563 + original_path: use_stmt.path.segments.iter() 564 + .map(|s| s.name.clone()) 565 + .chain(core::iter::once(type_name.clone())) 566 + .collect(), 567 + local_name: type_name.clone(), 568 + }; 569 + (type_name.clone(), imported) 570 + }) 571 + .collect() 572 + } 573 + UseImports::Items(items) => { 574 + let target_module = self.modules.get(&target_namespace).unwrap(); 575 + let mut imports = Vec::new(); 576 + 577 + let type_name = type_name_opt.as_ref().unwrap(); 578 + if !target_module.symbols.types.contains_key(type_name) { 579 + errors.push(ValidationError::UndefinedReference { 580 + name: alloc::format!("{}.{}", target_namespace, type_name), 581 + span: use_stmt.path.span, 582 + }); 583 + } else { 584 + for item in items { 585 + let local_name = if let Some(alias) = &item.alias { 586 + alias.name.clone() 587 + } else { 588 + type_name.clone() 589 + }; 590 + 591 + imports.push(( 592 + local_name.clone(), 593 + ImportedSymbol { 594 + original_path: use_stmt.path.segments.iter() 595 + .map(|s| s.name.clone()) 596 + .collect(), 597 + local_name, 598 + }, 599 + )); 600 + } 601 + } 602 + imports 603 + } 604 + }; 605 + 606 + let module = self.modules.get_mut(current_namespace).unwrap(); 607 + for (local_name, imported) in imports_to_add { 608 + module.imports.mappings.insert(local_name, imported); 609 + } 610 + 611 + if errors.is_empty() { 612 + Ok(()) 613 + } else { 614 + Err(errors) 615 + } 616 + } 617 + 618 + fn build_symbol_table(_namespace: &str, lexicon: &Lexicon) -> Result<SymbolTable, ValidationErrors> { 619 + let mut symbols = SymbolTable { 620 + types: BTreeMap::new(), 621 + }; 622 + let mut errors = ValidationErrors::new(); 623 + 624 + for item in &lexicon.items { 625 + match item { 626 + Item::Record(r) => { 627 + if let Some(existing) = symbols.types.get(&r.name.name) { 628 + errors.push(crate::error::ValidationError::DuplicateDefinition { 629 + name: r.name.name.clone(), 630 + first_span: existing.span(), 631 + second_span: r.name.span, 632 + }); 633 + } else { 634 + symbols.types.insert( 635 + r.name.name.clone(), 636 + Symbol::Record { 637 + name: r.name.name.clone(), 638 + span: r.name.span, 639 + }, 640 + ); 641 + } 642 + } 643 + Item::Alias(a) => { 644 + if let Some(existing) = symbols.types.get(&a.name.name) { 645 + errors.push(crate::error::ValidationError::DuplicateDefinition { 646 + name: a.name.name.clone(), 647 + first_span: existing.span(), 648 + second_span: a.name.span, 649 + }); 650 + } else { 651 + symbols.types.insert( 652 + a.name.name.clone(), 653 + Symbol::Alias { 654 + name: a.name.name.clone(), 655 + span: a.name.span, 656 + }, 657 + ); 658 + } 659 + } 660 + Item::Token(t) => { 661 + if let Some(existing) = symbols.types.get(&t.name.name) { 662 + errors.push(crate::error::ValidationError::DuplicateDefinition { 663 + name: t.name.name.clone(), 664 + first_span: existing.span(), 665 + second_span: t.name.span, 666 + }); 667 + } else { 668 + symbols.types.insert( 669 + t.name.name.clone(), 670 + Symbol::Token { 671 + name: t.name.name.clone(), 672 + span: t.name.span, 673 + }, 674 + ); 675 + } 676 + } 677 + Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 678 + // These don't define types, so skip 679 + } 680 + Item::Namespace(_) | Item::Use(_) => { 681 + // Handled separately 682 + } 683 + } 684 + } 685 + 686 + if errors.is_empty() { 687 + Ok(symbols) 688 + } else { 689 + Err(errors) 690 + } 691 + } 692 + 693 + fn resolve_module(&self, namespace: &str, module: &Module) -> Result<(), ValidationErrors> { 694 + let mut errors = ValidationErrors::new(); 695 + 696 + for item in &module.lexicon.items { 697 + if let Err(mut item_errors) = self.resolve_item(namespace, item) { 698 + errors.append(&mut item_errors); 699 + } 700 + } 701 + 702 + if errors.is_empty() { 703 + Ok(()) 704 + } else { 705 + Err(errors) 706 + } 707 + } 708 + 709 + fn resolve_item(&self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> { 710 + match item { 711 + Item::Record(r) => self.resolve_record(namespace, r), 712 + Item::Alias(a) => self.resolve_alias(namespace, a), 713 + Item::Query(q) => self.resolve_query(namespace, q), 714 + Item::Procedure(p) => self.resolve_procedure(namespace, p), 715 + Item::Subscription(s) => self.resolve_subscription(namespace, s), 716 + Item::Token(_) | Item::Namespace(_) | Item::Use(_) => Ok(()), 717 + } 718 + } 719 + 720 + fn resolve_record(&self, namespace: &str, record: &Record) -> Result<(), ValidationErrors> { 721 + let mut errors = ValidationErrors::new(); 722 + 723 + for field in &record.fields { 724 + if let Err(mut field_errors) = self.resolve_type(namespace, &field.ty) { 725 + errors.append(&mut field_errors); 726 + } 727 + } 728 + 729 + if errors.is_empty() { 730 + Ok(()) 731 + } else { 732 + Err(errors) 733 + } 734 + } 735 + 736 + fn resolve_alias(&self, namespace: &str, alias: &Alias) -> Result<(), ValidationErrors> { 737 + self.resolve_type(namespace, &alias.ty) 738 + } 739 + 740 + fn resolve_query(&self, namespace: &str, query: &Query) -> Result<(), ValidationErrors> { 741 + let mut errors = ValidationErrors::new(); 742 + 743 + for param in &query.params { 744 + if let Err(mut param_errors) = self.resolve_type(namespace, &param.ty) { 745 + errors.append(&mut param_errors); 746 + } 747 + } 748 + 749 + match &query.returns { 750 + ReturnType::Type(ty) => { 751 + if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { 752 + errors.append(&mut ty_errors); 753 + } 754 + } 755 + ReturnType::TypeWithErrors { success, .. } => { 756 + if let Err(mut ty_errors) = self.resolve_type(namespace, success) { 757 + errors.append(&mut ty_errors); 758 + } 759 + } 760 + } 761 + 762 + if errors.is_empty() { 763 + Ok(()) 764 + } else { 765 + Err(errors) 766 + } 767 + } 768 + 769 + fn resolve_procedure(&self, namespace: &str, procedure: &Procedure) -> Result<(), ValidationErrors> { 770 + let mut errors = ValidationErrors::new(); 771 + 772 + for param in &procedure.params { 773 + if let Err(mut param_errors) = self.resolve_type(namespace, &param.ty) { 774 + errors.append(&mut param_errors); 775 + } 776 + } 777 + 778 + match &procedure.returns { 779 + ReturnType::Type(ty) => { 780 + if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { 781 + errors.append(&mut ty_errors); 782 + } 783 + } 784 + ReturnType::TypeWithErrors { success, .. } => { 785 + if let Err(mut ty_errors) = self.resolve_type(namespace, success) { 786 + errors.append(&mut ty_errors); 787 + } 788 + } 789 + } 790 + 791 + if errors.is_empty() { 792 + Ok(()) 793 + } else { 794 + Err(errors) 795 + } 796 + } 797 + 798 + fn resolve_subscription(&self, namespace: &str, subscription: &Subscription) -> Result<(), ValidationErrors> { 799 + let mut errors = ValidationErrors::new(); 800 + 801 + for param in &subscription.params { 802 + if let Err(mut param_errors) = self.resolve_type(namespace, &param.ty) { 803 + errors.append(&mut param_errors); 804 + } 805 + } 806 + 807 + if let Err(mut msg_errors) = self.resolve_type(namespace, &subscription.messages) { 808 + errors.append(&mut msg_errors); 809 + } 810 + 811 + if errors.is_empty() { 812 + Ok(()) 813 + } else { 814 + Err(errors) 815 + } 816 + } 817 + 818 + fn resolve_type(&self, namespace: &str, ty: &Type) -> Result<(), ValidationErrors> { 819 + match ty { 820 + Type::Primitive { .. } | Type::Unknown { .. } => Ok(()), 821 + Type::Reference { path, span } => { 822 + self.resolve_reference(namespace, path, *span) 823 + } 824 + Type::Array { inner, .. } => { 825 + self.resolve_type(namespace, inner) 826 + } 827 + Type::Union { types, .. } => { 828 + let mut errors = ValidationErrors::new(); 829 + for ty in types { 830 + if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { 831 + errors.append(&mut ty_errors); 832 + } 833 + } 834 + if errors.is_empty() { 835 + Ok(()) 836 + } else { 837 + Err(errors) 838 + } 839 + } 840 + Type::Object { fields, .. } => { 841 + let mut errors = ValidationErrors::new(); 842 + for field in fields { 843 + if let Err(mut field_errors) = self.resolve_type(namespace, &field.ty) { 844 + errors.append(&mut field_errors); 845 + } 846 + } 847 + if errors.is_empty() { 848 + Ok(()) 849 + } else { 850 + Err(errors) 851 + } 852 + } 853 + Type::Constrained { base, .. } => { 854 + self.resolve_type(namespace, base) 855 + } 856 + } 857 + } 858 + 859 + fn resolve_reference(&self, current_namespace: &str, path: &Path, span: Span) -> Result<(), ValidationErrors> { 860 + let full_path = path.to_string(); 861 + 862 + if path.segments.len() == 1 { 863 + let name = &path.segments[0].name; 864 + 865 + if let Some(module) = self.modules.get(current_namespace) { 866 + if module.symbols.types.contains_key(name) { 867 + return Ok(()); 868 + } 869 + 870 + if module.imports.mappings.contains_key(name) { 871 + return Ok(()); 872 + } 873 + } 874 + 875 + if let Some(module) = self.modules.get("prelude") { 876 + if module.symbols.types.contains_key(name) { 877 + return Ok(()); 878 + } 879 + } 880 + 881 + let mut errors = ValidationErrors::new(); 882 + errors.push(crate::error::ValidationError::UndefinedReference { 883 + name: name.clone(), 884 + span, 885 + }); 886 + return Err(errors); 887 + } 888 + 889 + let target_namespace = path.segments[..path.segments.len() - 1] 890 + .iter() 891 + .map(|s| s.name.as_str()) 892 + .collect::<Vec<_>>() 893 + .join("."); 894 + let type_name = &path.segments[path.segments.len() - 1].name; 895 + 896 + if let Some(module) = self.modules.get(&target_namespace) { 897 + if module.symbols.types.contains_key(type_name) { 898 + return Ok(()); 899 + } 900 + } 901 + 902 + let mut errors = ValidationErrors::new(); 903 + errors.push(crate::error::ValidationError::UndefinedReference { 904 + name: full_path, 905 + span, 906 + }); 907 + Err(errors) 908 + } 909 + } 910 + 911 + impl Symbol { 912 + fn span(&self) -> Span { 913 + match self { 914 + Symbol::Record { span, .. } => *span, 915 + Symbol::Alias { span, .. } => *span, 916 + Symbol::Token { span, .. } => *span, 917 + } 918 + } 919 + } 920 + 921 + impl Default for Workspace { 922 + fn default() -> Self { 923 + Self::new() 924 + } 925 + } 926 + 927 + #[cfg(test)] 928 + mod tests { 929 + use super::*; 930 + use crate::parser::parse_lexicon; 931 + 932 + #[test] 933 + fn test_workspace_basic() { 934 + let mut ws = Workspace::new(); 935 + let input = "record foo { bar: string, };"; 936 + let lexicon = parse_lexicon(input).unwrap(); 937 + assert!(ws.add_module("test".into(), lexicon).is_ok()); 938 + } 939 + 940 + #[test] 941 + fn test_duplicate_definition() { 942 + let mut ws = Workspace::new(); 943 + let input = "record foo {}; alias foo = string;"; 944 + let lexicon = parse_lexicon(input).unwrap(); 945 + let result = ws.add_module("test".into(), lexicon); 946 + assert!(result.is_err()); 947 + } 948 + 949 + #[test] 950 + fn test_undefined_reference() { 951 + let mut ws = Workspace::new(); 952 + let input = "record foo { bar: unknown_type, };"; 953 + let lexicon = parse_lexicon(input).unwrap(); 954 + ws.add_module("test".into(), lexicon).unwrap(); 955 + let result = ws.resolve(); 956 + assert!(result.is_err()); 957 + } 958 + 959 + #[test] 960 + fn test_self_reference() { 961 + let mut ws = Workspace::new(); 962 + let input = "record foo { bar: foo, };"; 963 + let lexicon = parse_lexicon(input).unwrap(); 964 + ws.add_module("test".into(), lexicon).unwrap(); 965 + assert!(ws.resolve().is_ok()); 966 + } 967 + 968 + #[test] 969 + fn test_cross_module_reference() { 970 + let mut ws = Workspace::new(); 971 + 972 + let a = parse_lexicon("record foo {};").unwrap(); 973 + ws.add_module("a".into(), a).unwrap(); 974 + 975 + let b = parse_lexicon("record bar { baz: a.foo, };").unwrap(); 976 + ws.add_module("b".into(), b).unwrap(); 977 + 978 + assert!(ws.resolve().is_ok()); 979 + } 980 + 981 + #[test] 982 + fn test_prelude_reference() { 983 + let ws = Workspace::with_prelude().unwrap(); 984 + 985 + // Prelude should be accessible 986 + assert!(ws.modules.contains_key("prelude")); 987 + } 988 + 989 + #[test] 990 + fn test_use_import_all() { 991 + let mut ws = Workspace::new(); 992 + 993 + let a = parse_lexicon("record foo {}; alias bar = string;").unwrap(); 994 + ws.add_module("a".into(), a).unwrap(); 995 + 996 + let b = parse_lexicon("use a; record baz { x: foo, y: bar, };").unwrap(); 997 + ws.add_module("b".into(), b).unwrap(); 998 + 999 + assert!(ws.resolve().is_ok()); 1000 + } 1001 + 1002 + #[test] 1003 + fn test_use_import_with_alias() { 1004 + let mut ws = Workspace::new(); 1005 + 1006 + let a = parse_lexicon("record post {};").unwrap(); 1007 + ws.add_module("app.bsky.feed".into(), a).unwrap(); 1008 + 1009 + let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, };").unwrap(); 1010 + ws.add_module("app.bsky.feed.like".into(), b).unwrap(); 1011 + 1012 + let result = ws.resolve(); 1013 + if let Err(ref e) = result { 1014 + eprintln!("Errors: {:?}", e); 1015 + } 1016 + assert!(result.is_ok()); 1017 + } 1018 + 1019 + #[test] 1020 + fn test_use_undefined_module() { 1021 + let mut ws = Workspace::new(); 1022 + 1023 + let a = parse_lexicon("use nonexistent; record foo {};").unwrap(); 1024 + ws.add_module("test".into(), a).unwrap(); 1025 + 1026 + let result = ws.resolve(); 1027 + assert!(result.is_err()); 1028 + } 1029 + 1030 + #[test] 1031 + fn test_use_undefined_type() { 1032 + let mut ws = Workspace::new(); 1033 + 1034 + let a = parse_lexicon("record foo {};").unwrap(); 1035 + ws.add_module("a".into(), a).unwrap(); 1036 + 1037 + let b = parse_lexicon("use a.bar; record baz {};").unwrap(); 1038 + ws.add_module("b".into(), b).unwrap(); 1039 + 1040 + let result = ws.resolve(); 1041 + assert!(result.is_err()); 1042 + } 1043 + 1044 + #[test] 1045 + fn test_typecheck_string_constraint_on_string() { 1046 + let mut ws = Workspace::new(); 1047 + 1048 + let input = r#"alias shortString = string constrained { 1049 + maxLength: 100, 1050 + };"#; 1051 + let lexicon = parse_lexicon(input).unwrap(); 1052 + ws.add_module("test".into(), lexicon).unwrap(); 1053 + 1054 + assert!(ws.resolve().is_ok()); 1055 + } 1056 + 1057 + #[test] 1058 + fn test_typecheck_string_constraint_on_integer() { 1059 + let mut ws = Workspace::new(); 1060 + 1061 + let input = r#"alias constrainedInt = integer constrained { 1062 + maxLength: 100, 1063 + };"#; 1064 + let lexicon = parse_lexicon(input).unwrap(); 1065 + ws.add_module("test".into(), lexicon).unwrap(); 1066 + 1067 + let result = ws.resolve(); 1068 + assert!(result.is_err()); 1069 + } 1070 + 1071 + #[test] 1072 + fn test_typecheck_numeric_constraint_on_integer() { 1073 + let mut ws = Workspace::new(); 1074 + 1075 + let input = r#"alias positiveInt = integer constrained { 1076 + minimum: 0, 1077 + };"#; 1078 + let lexicon = parse_lexicon(input).unwrap(); 1079 + ws.add_module("test".into(), lexicon).unwrap(); 1080 + 1081 + assert!(ws.resolve().is_ok()); 1082 + } 1083 + 1084 + #[test] 1085 + fn test_typecheck_numeric_constraint_on_string() { 1086 + let mut ws = Workspace::new(); 1087 + 1088 + let input = r#"alias constrainedString = string constrained { 1089 + minimum: 0, 1090 + };"#; 1091 + let lexicon = parse_lexicon(input).unwrap(); 1092 + ws.add_module("test".into(), lexicon).unwrap(); 1093 + 1094 + let result = ws.resolve(); 1095 + assert!(result.is_err()); 1096 + } 1097 + 1098 + #[test] 1099 + fn test_typecheck_constrained_alias() { 1100 + let mut ws = Workspace::new(); 1101 + 1102 + let input = r#" 1103 + alias shortString = string constrained { 1104 + maxLength: 100, 1105 + }; 1106 + 1107 + alias tinyString = shortString constrained { 1108 + maxLength: 50, 1109 + }; 1110 + "#; 1111 + let lexicon = parse_lexicon(input).unwrap(); 1112 + ws.add_module("test".into(), lexicon).unwrap(); 1113 + 1114 + assert!(ws.resolve().is_ok()); 1115 + } 1116 + 1117 + #[test] 1118 + fn test_constraint_refinement_maxlength_valid() { 1119 + let mut ws = Workspace::new(); 1120 + 1121 + let input = r#" 1122 + alias shortString = string constrained { 1123 + maxLength: 100, 1124 + }; 1125 + 1126 + alias tinyString = shortString constrained { 1127 + maxLength: 50, 1128 + }; 1129 + "#; 1130 + let lexicon = parse_lexicon(input).unwrap(); 1131 + ws.add_module("test".into(), lexicon).unwrap(); 1132 + 1133 + assert!(ws.resolve().is_ok()); 1134 + } 1135 + 1136 + #[test] 1137 + fn test_constraint_refinement_maxlength_invalid() { 1138 + let mut ws = Workspace::new(); 1139 + 1140 + let input = r#" 1141 + alias shortString = string constrained { 1142 + maxLength: 50, 1143 + }; 1144 + 1145 + alias longString = shortString constrained { 1146 + maxLength: 100, 1147 + }; 1148 + "#; 1149 + let lexicon = parse_lexicon(input).unwrap(); 1150 + ws.add_module("test".into(), lexicon).unwrap(); 1151 + 1152 + let result = ws.resolve(); 1153 + assert!(result.is_err()); 1154 + let errors = result.unwrap_err(); 1155 + assert!(errors.errors.iter().any(|e| matches!(e, ValidationError::ConstraintTooPermissive { .. }))); 1156 + } 1157 + 1158 + #[test] 1159 + fn test_constraint_refinement_minlength_valid() { 1160 + let mut ws = Workspace::new(); 1161 + 1162 + let input = r#" 1163 + alias minString = string constrained { 1164 + minLength: 10, 1165 + }; 1166 + 1167 + alias longerMinString = minString constrained { 1168 + minLength: 20, 1169 + }; 1170 + "#; 1171 + let lexicon = parse_lexicon(input).unwrap(); 1172 + ws.add_module("test".into(), lexicon).unwrap(); 1173 + 1174 + assert!(ws.resolve().is_ok()); 1175 + } 1176 + 1177 + #[test] 1178 + fn test_constraint_refinement_minlength_invalid() { 1179 + let mut ws = Workspace::new(); 1180 + 1181 + let input = r#" 1182 + alias minString = string constrained { 1183 + minLength: 20, 1184 + }; 1185 + 1186 + alias shorterMinString = minString constrained { 1187 + minLength: 10, 1188 + }; 1189 + "#; 1190 + let lexicon = parse_lexicon(input).unwrap(); 1191 + ws.add_module("test".into(), lexicon).unwrap(); 1192 + 1193 + let result = ws.resolve(); 1194 + assert!(result.is_err()); 1195 + } 1196 + 1197 + #[test] 1198 + fn test_constraint_refinement_maximum_valid() { 1199 + let mut ws = Workspace::new(); 1200 + 1201 + let input = r#" 1202 + alias largeInt = integer constrained { 1203 + maximum: 1000, 1204 + }; 1205 + 1206 + alias smallInt = largeInt constrained { 1207 + maximum: 100, 1208 + }; 1209 + "#; 1210 + let lexicon = parse_lexicon(input).unwrap(); 1211 + ws.add_module("test".into(), lexicon).unwrap(); 1212 + 1213 + assert!(ws.resolve().is_ok()); 1214 + } 1215 + 1216 + #[test] 1217 + fn test_constraint_refinement_maximum_invalid() { 1218 + let mut ws = Workspace::new(); 1219 + 1220 + let input = r#" 1221 + alias smallInt = integer constrained { 1222 + maximum: 100, 1223 + }; 1224 + 1225 + alias largeInt = smallInt constrained { 1226 + maximum: 1000, 1227 + }; 1228 + "#; 1229 + let lexicon = parse_lexicon(input).unwrap(); 1230 + ws.add_module("test".into(), lexicon).unwrap(); 1231 + 1232 + let result = ws.resolve(); 1233 + assert!(result.is_err()); 1234 + } 1235 + 1236 + #[test] 1237 + fn test_constraint_refinement_minimum_valid() { 1238 + let mut ws = Workspace::new(); 1239 + 1240 + let input = r#" 1241 + alias positiveInt = integer constrained { 1242 + minimum: 0, 1243 + }; 1244 + 1245 + alias strictPositiveInt = positiveInt constrained { 1246 + minimum: 1, 1247 + }; 1248 + "#; 1249 + let lexicon = parse_lexicon(input).unwrap(); 1250 + ws.add_module("test".into(), lexicon).unwrap(); 1251 + 1252 + assert!(ws.resolve().is_ok()); 1253 + } 1254 + 1255 + #[test] 1256 + fn test_constraint_refinement_minimum_invalid() { 1257 + let mut ws = Workspace::new(); 1258 + 1259 + let input = r#" 1260 + alias positiveInt = integer constrained { 1261 + minimum: 10, 1262 + }; 1263 + 1264 + alias lowerInt = positiveInt constrained { 1265 + minimum: 5, 1266 + }; 1267 + "#; 1268 + let lexicon = parse_lexicon(input).unwrap(); 1269 + ws.add_module("test".into(), lexicon).unwrap(); 1270 + 1271 + let result = ws.resolve(); 1272 + assert!(result.is_err()); 1273 + } 1274 + 1275 + #[test] 1276 + fn test_constraint_refinement_enum_valid() { 1277 + let mut ws = Workspace::new(); 1278 + 1279 + let input = r#" 1280 + alias colorEnum = string constrained { 1281 + enum: ["red", "green", "blue"], 1282 + }; 1283 + 1284 + alias primaryColors = colorEnum constrained { 1285 + enum: ["red", "blue"], 1286 + }; 1287 + "#; 1288 + let lexicon = parse_lexicon(input).unwrap(); 1289 + ws.add_module("test".into(), lexicon).unwrap(); 1290 + 1291 + assert!(ws.resolve().is_ok()); 1292 + } 1293 + 1294 + #[test] 1295 + fn test_constraint_refinement_enum_invalid() { 1296 + let mut ws = Workspace::new(); 1297 + 1298 + let input = r#" 1299 + alias primaryColors = string constrained { 1300 + enum: ["red", "blue"], 1301 + }; 1302 + 1303 + alias moreColors = primaryColors constrained { 1304 + enum: ["red", "blue", "green"], 1305 + }; 1306 + "#; 1307 + let lexicon = parse_lexicon(input).unwrap(); 1308 + ws.add_module("test".into(), lexicon).unwrap(); 1309 + 1310 + let result = ws.resolve(); 1311 + assert!(result.is_err()); 1312 + } 1313 + 1314 + #[test] 1315 + fn test_constraint_refinement_chain() { 1316 + let mut ws = Workspace::new(); 1317 + 1318 + let input = r#" 1319 + alias baseString = string constrained { 1320 + maxLength: 1000, 1321 + minLength: 10, 1322 + }; 1323 + 1324 + alias mediumString = baseString constrained { 1325 + maxLength: 500, 1326 + minLength: 20, 1327 + }; 1328 + 1329 + alias shortString = mediumString constrained { 1330 + maxLength: 100, 1331 + minLength: 30, 1332 + }; 1333 + "#; 1334 + let lexicon = parse_lexicon(input).unwrap(); 1335 + ws.add_module("test".into(), lexicon).unwrap(); 1336 + 1337 + assert!(ws.resolve().is_ok()); 1338 + } 1339 + 1340 + #[test] 1341 + fn test_constraint_refinement_chain_invalid() { 1342 + let mut ws = Workspace::new(); 1343 + 1344 + let input = r#" 1345 + alias baseString = string constrained { 1346 + maxLength: 1000, 1347 + }; 1348 + 1349 + alias mediumString = baseString constrained { 1350 + maxLength: 500, 1351 + }; 1352 + 1353 + alias breakChain = mediumString constrained { 1354 + maxLength: 600, 1355 + }; 1356 + "#; 1357 + let lexicon = parse_lexicon(input).unwrap(); 1358 + ws.add_module("test".into(), lexicon).unwrap(); 1359 + 1360 + let result = ws.resolve(); 1361 + assert!(result.is_err()); 1362 + } 1363 + }
+8
mlf-validation/Cargo.toml
··· 1 + [package] 2 + name = "mlf-validation" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + mlf-lang = { path = "../mlf-lang" } 8 + serde_json = "1"
+565
mlf-validation/src/lib.rs
··· 1 + use mlf_lang::ast::*; 2 + use serde_json::Value as JsonValue; 3 + use std::fmt; 4 + 5 + #[derive(Debug, Clone)] 6 + pub struct ValidationError { 7 + pub path: String, 8 + pub message: String, 9 + } 10 + 11 + impl fmt::Display for ValidationError { 12 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 + write!(f, "{}: {}", self.path, self.message) 14 + } 15 + } 16 + 17 + impl std::error::Error for ValidationError {} 18 + 19 + pub struct RecordValidator<'a> { 20 + lexicon: &'a Lexicon, 21 + } 22 + 23 + impl<'a> RecordValidator<'a> { 24 + pub fn new(lexicon: &'a Lexicon) -> Self { 25 + Self { lexicon } 26 + } 27 + 28 + pub fn validate_record(&self, record: &JsonValue) -> Result<(), Vec<ValidationError>> { 29 + let mut errors = Vec::new(); 30 + 31 + // Find the main record definition 32 + let main_item = self.find_main_item()?; 33 + 34 + match main_item { 35 + Item::Record(record_def) => { 36 + // Validate as object with the record's fields 37 + self.validate_object(record, &record_def.fields, "$", &mut errors); 38 + } 39 + Item::Query(_) | Item::Procedure(_) => { 40 + errors.push(ValidationError { 41 + path: "$".to_string(), 42 + message: "Cannot validate records against query/procedure definitions".to_string(), 43 + }); 44 + } 45 + _ => { 46 + errors.push(ValidationError { 47 + path: "$".to_string(), 48 + message: "No record definition found in lexicon".to_string(), 49 + }); 50 + } 51 + } 52 + 53 + if errors.is_empty() { 54 + Ok(()) 55 + } else { 56 + Err(errors) 57 + } 58 + } 59 + 60 + fn find_main_item(&self) -> Result<&Item, Vec<ValidationError>> { 61 + // Look for a record, query, or procedure (main definitions) 62 + for item in &self.lexicon.items { 63 + match item { 64 + Item::Record(_) | Item::Query(_) | Item::Procedure(_) => { 65 + return Ok(item); 66 + } 67 + _ => continue, 68 + } 69 + } 70 + Err(vec![ValidationError { 71 + path: "$".to_string(), 72 + message: "No main definition found in lexicon".to_string(), 73 + }]) 74 + } 75 + 76 + fn validate_against_type( 77 + &self, 78 + value: &JsonValue, 79 + ty: &Type, 80 + path: &str, 81 + errors: &mut Vec<ValidationError>, 82 + ) { 83 + match ty { 84 + Type::Primitive { kind, .. } => { 85 + self.validate_primitive(value, *kind, path, errors); 86 + } 87 + Type::Constrained { base, constraints, .. } => { 88 + self.validate_against_type(value, base, path, errors); 89 + self.validate_constraints(value, constraints, path, errors); 90 + } 91 + Type::Object { fields, .. } => { 92 + self.validate_object(value, fields, path, errors); 93 + } 94 + Type::Array { inner, .. } => { 95 + self.validate_array(value, inner, path, errors); 96 + } 97 + Type::Union { types, .. } => { 98 + self.validate_union(value, types, path, errors); 99 + } 100 + Type::Reference { path: ref_path, .. } => { 101 + // Try to resolve reference 102 + if let Some(resolved_type) = self.resolve_reference(ref_path) { 103 + self.validate_against_type(value, &resolved_type, path, errors); 104 + } else { 105 + // Can't resolve, skip validation 106 + } 107 + } 108 + Type::Unknown { .. } => { 109 + // Unknown type accepts anything 110 + } 111 + } 112 + } 113 + 114 + fn resolve_reference(&self, path: &Path) -> Option<Type> { 115 + // Simple resolution: look for aliases with matching name 116 + if path.segments.len() == 1 { 117 + let name = &path.segments[0].name; 118 + 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 + } 123 + } 124 + } 125 + } 126 + None 127 + } 128 + 129 + fn validate_primitive( 130 + &self, 131 + value: &JsonValue, 132 + kind: PrimitiveType, 133 + path: &str, 134 + errors: &mut Vec<ValidationError>, 135 + ) { 136 + match kind { 137 + PrimitiveType::Null => { 138 + if !value.is_null() { 139 + errors.push(ValidationError { 140 + path: path.to_string(), 141 + message: "Expected null".to_string(), 142 + }); 143 + } 144 + } 145 + PrimitiveType::Boolean => { 146 + if !value.is_boolean() { 147 + errors.push(ValidationError { 148 + path: path.to_string(), 149 + message: "Expected boolean".to_string(), 150 + }); 151 + } 152 + } 153 + PrimitiveType::Integer => { 154 + if let Some(n) = value.as_i64() { 155 + // Check JavaScript-safe integer range (-2^53 to 2^53) 156 + if n < -(1i64 << 53) || n > (1i64 << 53) { 157 + errors.push(ValidationError { 158 + path: path.to_string(), 159 + message: "Integer out of JavaScript-safe range".to_string(), 160 + }); 161 + } 162 + } else { 163 + errors.push(ValidationError { 164 + path: path.to_string(), 165 + message: "Expected integer".to_string(), 166 + }); 167 + } 168 + } 169 + PrimitiveType::Number => { 170 + if !value.is_f64() && !value.is_i64() { 171 + errors.push(ValidationError { 172 + path: path.to_string(), 173 + message: "Expected number".to_string(), 174 + }); 175 + } 176 + } 177 + PrimitiveType::String => { 178 + if !value.is_string() { 179 + errors.push(ValidationError { 180 + path: path.to_string(), 181 + message: "Expected string".to_string(), 182 + }); 183 + } 184 + } 185 + PrimitiveType::Bytes => { 186 + // Bytes should be encoded as {"$bytes": "base64-string"} 187 + if let Some(obj) = value.as_object() { 188 + if let Some(bytes_val) = obj.get("$bytes") { 189 + if !bytes_val.is_string() { 190 + errors.push(ValidationError { 191 + path: path.to_string(), 192 + message: "Expected $bytes to be a base64 string".to_string(), 193 + }); 194 + } 195 + } else { 196 + errors.push(ValidationError { 197 + path: path.to_string(), 198 + message: "Expected object with $bytes field".to_string(), 199 + }); 200 + } 201 + } else { 202 + errors.push(ValidationError { 203 + path: path.to_string(), 204 + message: "Expected bytes object with $bytes field".to_string(), 205 + }); 206 + } 207 + } 208 + PrimitiveType::Blob => { 209 + // Blob should have $type, ref, mimeType, size 210 + if let Some(obj) = value.as_object() { 211 + let required = ["$type", "ref", "mimeType", "size"]; 212 + for field in &required { 213 + if !obj.contains_key(*field) { 214 + errors.push(ValidationError { 215 + path: path.to_string(), 216 + message: format!("Blob missing required field: {}", field), 217 + }); 218 + } 219 + } 220 + } else { 221 + errors.push(ValidationError { 222 + path: path.to_string(), 223 + message: "Expected blob object".to_string(), 224 + }); 225 + } 226 + } 227 + } 228 + } 229 + 230 + fn validate_constraints( 231 + &self, 232 + value: &JsonValue, 233 + constraints: &[Constraint], 234 + path: &str, 235 + errors: &mut Vec<ValidationError>, 236 + ) { 237 + for constraint in constraints { 238 + match constraint { 239 + Constraint::MinLength { value: min, .. } => { 240 + if let Some(s) = value.as_str() { 241 + if s.len() < *min { 242 + errors.push(ValidationError { 243 + path: path.to_string(), 244 + message: format!("String too short: {} bytes (min: {})", s.len(), min), 245 + }); 246 + } 247 + } 248 + } 249 + Constraint::MaxLength { value: max, .. } => { 250 + if let Some(s) = value.as_str() { 251 + if s.len() > *max { 252 + errors.push(ValidationError { 253 + path: path.to_string(), 254 + message: format!("String too long: {} bytes (max: {})", s.len(), max), 255 + }); 256 + } 257 + } 258 + } 259 + Constraint::MinGraphemes { value: min, .. } => { 260 + if let Some(s) = value.as_str() { 261 + let count = s.chars().count(); // Simplified grapheme count 262 + if count < *min { 263 + errors.push(ValidationError { 264 + path: path.to_string(), 265 + message: format!("String has too few graphemes: {} (min: {})", count, min), 266 + }); 267 + } 268 + } 269 + } 270 + Constraint::MaxGraphemes { value: max, .. } => { 271 + if let Some(s) = value.as_str() { 272 + let count = s.chars().count(); // Simplified grapheme count 273 + if count > *max { 274 + errors.push(ValidationError { 275 + path: path.to_string(), 276 + message: format!("String has too many graphemes: {} (max: {})", count, max), 277 + }); 278 + } 279 + } 280 + } 281 + Constraint::Minimum { value: min, .. } => { 282 + if let Some(n) = value.as_i64() { 283 + if n < *min { 284 + errors.push(ValidationError { 285 + path: path.to_string(), 286 + message: format!("Value too small: {} (min: {})", n, min), 287 + }); 288 + } 289 + } 290 + } 291 + Constraint::Maximum { value: max, .. } => { 292 + if let Some(n) = value.as_i64() { 293 + if n > *max { 294 + errors.push(ValidationError { 295 + path: path.to_string(), 296 + message: format!("Value too large: {} (max: {})", n, max), 297 + }); 298 + } 299 + } 300 + } 301 + Constraint::Enum { values, .. } => { 302 + if let Some(s) = value.as_str() { 303 + if !values.contains(&s.to_string()) { 304 + errors.push(ValidationError { 305 + path: path.to_string(), 306 + message: format!("Value '{}' not in enum: {:?}", s, values), 307 + }); 308 + } 309 + } 310 + } 311 + Constraint::Format { value: format, .. } => { 312 + if let Some(s) = value.as_str() { 313 + self.validate_format(s, format, path, errors); 314 + } 315 + } 316 + Constraint::Accept { mimes, .. } => { 317 + // Validate blob mimeType against accept list 318 + if let Some(obj) = value.as_object() { 319 + if let Some(mime) = obj.get("mimeType").and_then(|v| v.as_str()) { 320 + if !mimes.iter().any(|m| m == mime) { 321 + errors.push(ValidationError { 322 + path: path.to_string(), 323 + message: format!("MIME type '{}' not accepted (allowed: {:?})", mime, mimes), 324 + }); 325 + } 326 + } 327 + } 328 + } 329 + Constraint::MaxSize { value: max, .. } => { 330 + // Validate blob size 331 + if let Some(obj) = value.as_object() { 332 + if let Some(size) = obj.get("size").and_then(|v| v.as_u64()) { 333 + if size as usize > *max { 334 + errors.push(ValidationError { 335 + path: path.to_string(), 336 + message: format!("Blob size {} exceeds maximum: {}", size, max), 337 + }); 338 + } 339 + } 340 + } 341 + } 342 + Constraint::KnownValues { .. } => { 343 + // knownValues is a hint, not enforced 344 + } 345 + Constraint::Default { .. } => { 346 + // Default values are used when field is missing, not for validation 347 + } 348 + } 349 + } 350 + } 351 + 352 + fn validate_format( 353 + &self, 354 + value: &str, 355 + format: &str, 356 + path: &str, 357 + errors: &mut Vec<ValidationError>, 358 + ) { 359 + // TODO: Implement proper format validation according to ATProto spec 360 + // These are currently basic checks and should be replaced with proper validation: 361 + // - datetime: RFC 3339 datetime validation 362 + // - uri: RFC 3986 URI validation 363 + // - at-uri: ATProto AT-URI validation 364 + // - did: DID spec validation (did:plc:, did:web:, etc.) 365 + // - handle: ATProto handle validation 366 + // - nsid: NSID validation (reverse domain name) 367 + // - cid: CID validation (multibase-encoded multihash) 368 + // - at-identifier: either a DID or handle 369 + // - language: BCP 47 language tag validation 370 + // - tid: TID validation (base32-sortable timestamp) 371 + // - record-key: Record key validation 372 + 373 + match format { 374 + "datetime" => { 375 + // Basic ISO 8601 datetime validation 376 + if !value.contains('T') && !value.contains('Z') && !value.contains('+') && !value.contains('-') { 377 + errors.push(ValidationError { 378 + path: path.to_string(), 379 + message: format!("Invalid datetime format: '{}' (expected ISO 8601)", value), 380 + }); 381 + } 382 + } 383 + "uri" => { 384 + if !value.starts_with("http://") && !value.starts_with("https://") && !value.starts_with("ftp://") { 385 + errors.push(ValidationError { 386 + path: path.to_string(), 387 + message: format!("Invalid URI: '{}' (expected http://, https://, or ftp://)", value), 388 + }); 389 + } 390 + } 391 + "at-uri" => { 392 + if !value.starts_with("at://") { 393 + errors.push(ValidationError { 394 + path: path.to_string(), 395 + message: format!("Invalid AT-URI: '{}' (expected at://)", value), 396 + }); 397 + } 398 + } 399 + "did" => { 400 + if !value.starts_with("did:") { 401 + errors.push(ValidationError { 402 + path: path.to_string(), 403 + message: format!("Invalid DID: '{}' (expected did:)", value), 404 + }); 405 + } 406 + } 407 + "handle" => { 408 + // Basic handle validation (domain-like) 409 + if !value.contains('.') || value.starts_with('.') || value.ends_with('.') { 410 + errors.push(ValidationError { 411 + path: path.to_string(), 412 + message: format!("Invalid handle: '{}' (expected domain format)", value), 413 + }); 414 + } 415 + } 416 + "nsid" => { 417 + // NSID format: authority.name(.name)* 418 + let parts: Vec<&str> = value.split('.').collect(); 419 + if parts.len() < 2 { 420 + errors.push(ValidationError { 421 + path: path.to_string(), 422 + message: format!("Invalid NSID: '{}' (expected at least 2 segments)", value), 423 + }); 424 + } 425 + } 426 + "cid" => { 427 + // CID validation is complex, just check not empty 428 + if value.is_empty() { 429 + errors.push(ValidationError { 430 + path: path.to_string(), 431 + message: "Invalid CID: empty string".to_string(), 432 + }); 433 + } 434 + } 435 + "at-identifier" => { 436 + // Can be either DID or handle 437 + let is_did = value.starts_with("did:"); 438 + let is_handle = value.contains('.') && !value.starts_with('.') && !value.ends_with('.'); 439 + 440 + if !is_did && !is_handle { 441 + errors.push(ValidationError { 442 + path: path.to_string(), 443 + message: format!("Invalid AT-identifier: '{}' (expected DID or handle)", value), 444 + }); 445 + } 446 + } 447 + "language" => { 448 + // Basic BCP 47 validation (simplified) 449 + if value.len() < 2 || !value.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { 450 + errors.push(ValidationError { 451 + path: path.to_string(), 452 + message: format!("Invalid language code: '{}'", value), 453 + }); 454 + } 455 + } 456 + "tid" | "record-key" => { 457 + // Basic validation for TID and record-key 458 + if value.is_empty() { 459 + errors.push(ValidationError { 460 + path: path.to_string(), 461 + message: format!("Invalid {}: empty string", format), 462 + }); 463 + } 464 + } 465 + _ => { 466 + // Unknown format, skip validation 467 + } 468 + } 469 + } 470 + 471 + fn validate_object( 472 + &self, 473 + value: &JsonValue, 474 + fields: &[Field], 475 + path: &str, 476 + errors: &mut Vec<ValidationError>, 477 + ) { 478 + if let Some(obj) = value.as_object() { 479 + // Check required fields 480 + for field in fields { 481 + if !field.optional && !obj.contains_key(&field.name.name) { 482 + errors.push(ValidationError { 483 + path: if path == "$" { 484 + field.name.name.clone() 485 + } else { 486 + format!("{}.{}", path, field.name.name) 487 + }, 488 + message: "Required field missing".to_string(), 489 + }); 490 + } else if let Some(field_value) = obj.get(&field.name.name) { 491 + let field_path = if path == "$" { 492 + field.name.name.clone() 493 + } else { 494 + format!("{}.{}", path, field.name.name) 495 + }; 496 + self.validate_against_type(field_value, &field.ty, &field_path, errors); 497 + } 498 + } 499 + } else { 500 + errors.push(ValidationError { 501 + path: path.to_string(), 502 + message: format!("Expected object, got {}", value_type_name(value)), 503 + }); 504 + } 505 + } 506 + 507 + fn validate_array( 508 + &self, 509 + value: &JsonValue, 510 + inner: &Type, 511 + path: &str, 512 + errors: &mut Vec<ValidationError>, 513 + ) { 514 + if let Some(arr) = value.as_array() { 515 + for (i, item) in arr.iter().enumerate() { 516 + let item_path = format!("{}[{}]", path, i); 517 + self.validate_against_type(item, inner, &item_path, errors); 518 + } 519 + } else { 520 + errors.push(ValidationError { 521 + path: path.to_string(), 522 + message: format!("Expected array, got {}", value_type_name(value)), 523 + }); 524 + } 525 + } 526 + 527 + fn validate_union( 528 + &self, 529 + value: &JsonValue, 530 + types: &[Type], 531 + path: &str, 532 + errors: &mut Vec<ValidationError>, 533 + ) { 534 + // Try to validate against each type in the union 535 + let mut matched = false; 536 + 537 + for ty in types { 538 + let mut type_errors = Vec::new(); 539 + self.validate_against_type(value, ty, path, &mut type_errors); 540 + 541 + if type_errors.is_empty() { 542 + matched = true; 543 + break; 544 + } 545 + } 546 + 547 + if !matched { 548 + errors.push(ValidationError { 549 + path: path.to_string(), 550 + message: format!("Value does not match any type in union ({} variants tried)", types.len()), 551 + }); 552 + } 553 + } 554 + } 555 + 556 + fn value_type_name(value: &JsonValue) -> &'static str { 557 + match value { 558 + JsonValue::Null => "null", 559 + JsonValue::Bool(_) => "boolean", 560 + JsonValue::Number(_) => "number", 561 + JsonValue::String(_) => "string", 562 + JsonValue::Array(_) => "array", 563 + JsonValue::Object(_) => "object", 564 + } 565 + }
+19
mlf-wasm/Cargo.toml
··· 1 + [package] 2 + name = "mlf-wasm" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [lib] 7 + crate-type = ["cdylib", "rlib"] 8 + 9 + [dependencies] 10 + mlf-lang = { path = "../mlf-lang" } 11 + mlf-validation = { path = "../mlf-validation" } 12 + mlf-codegen = { path = "../mlf-codegen" } 13 + wasm-bindgen = "0.2" 14 + serde = { version = "1", features = ["derive"] } 15 + serde_json = "1" 16 + serde-wasm-bindgen = "0.6" 17 + 18 + [dev-dependencies] 19 + wasm-bindgen-test = "0.3"
+202
mlf-wasm/src/lib.rs
··· 1 + use wasm_bindgen::prelude::*; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + #[derive(Serialize, Deserialize)] 5 + pub struct ParseResult { 6 + pub success: bool, 7 + pub error: Option<String>, 8 + } 9 + 10 + #[derive(Serialize, Deserialize)] 11 + pub struct CheckResult { 12 + pub success: bool, 13 + pub errors: Vec<String>, 14 + } 15 + 16 + #[derive(Serialize, Deserialize)] 17 + pub struct GenerateResult { 18 + pub success: bool, 19 + pub lexicon: Option<String>, 20 + pub error: Option<String>, 21 + } 22 + 23 + #[derive(Serialize, Deserialize)] 24 + pub struct ValidateResult { 25 + pub success: bool, 26 + pub errors: Vec<String>, 27 + } 28 + 29 + /// Parse MLF source code and return whether it's valid 30 + #[wasm_bindgen] 31 + pub fn parse(source: &str) -> JsValue { 32 + let result = match mlf_lang::parse_lexicon(source) { 33 + Ok(_) => ParseResult { 34 + success: true, 35 + error: None, 36 + }, 37 + Err(e) => ParseResult { 38 + success: false, 39 + error: Some(format!("{:?}", e)), 40 + }, 41 + }; 42 + 43 + serde_wasm_bindgen::to_value(&result).unwrap() 44 + } 45 + 46 + /// Check MLF source for errors (parse + validate) 47 + #[wasm_bindgen] 48 + pub fn check(source: &str) -> JsValue { 49 + let mut errors = Vec::new(); 50 + 51 + // Parse the lexicon 52 + let lexicon = match mlf_lang::parse_lexicon(source) { 53 + Ok(lex) => lex, 54 + Err(e) => { 55 + errors.push(format!("Parse error: {:?}", e)); 56 + let result = CheckResult { 57 + success: false, 58 + errors, 59 + }; 60 + return serde_wasm_bindgen::to_value(&result).unwrap(); 61 + } 62 + }; 63 + 64 + // Create workspace and validate 65 + let mut workspace = match mlf_lang::Workspace::with_prelude() { 66 + Ok(ws) => ws, 67 + Err(e) => { 68 + errors.push(format!("Failed to load prelude: {:?}", e)); 69 + let result = CheckResult { 70 + success: false, 71 + errors, 72 + }; 73 + return serde_wasm_bindgen::to_value(&result).unwrap(); 74 + } 75 + }; 76 + 77 + if let Err(e) = workspace.add_module("main".to_string(), lexicon) { 78 + errors.push(format!("Validation error: {:?}", e)); 79 + } 80 + 81 + if let Err(e) = workspace.resolve() { 82 + errors.push(format!("Resolution error: {:?}", e)); 83 + } 84 + 85 + let result = CheckResult { 86 + success: errors.is_empty(), 87 + errors, 88 + }; 89 + 90 + serde_wasm_bindgen::to_value(&result).unwrap() 91 + } 92 + 93 + /// Generate JSON lexicon from MLF source 94 + #[wasm_bindgen] 95 + pub fn generate_lexicon(source: &str, namespace: &str) -> JsValue { 96 + // Parse the lexicon 97 + let lexicon = match mlf_lang::parse_lexicon(source) { 98 + Ok(lex) => lex, 99 + Err(e) => { 100 + let result = GenerateResult { 101 + success: false, 102 + lexicon: None, 103 + error: Some(format!("Parse error: {:?}", e)), 104 + }; 105 + return serde_wasm_bindgen::to_value(&result).unwrap(); 106 + } 107 + }; 108 + 109 + // Generate JSON lexicon 110 + let json_lexicon = mlf_codegen::generate_lexicon(namespace, &lexicon); 111 + 112 + match serde_json::to_string_pretty(&json_lexicon) { 113 + Ok(json_str) => { 114 + let result = GenerateResult { 115 + success: true, 116 + lexicon: Some(json_str), 117 + error: None, 118 + }; 119 + serde_wasm_bindgen::to_value(&result).unwrap() 120 + } 121 + Err(e) => { 122 + let result = GenerateResult { 123 + success: false, 124 + lexicon: None, 125 + error: Some(format!("JSON serialization error: {}", e)), 126 + }; 127 + serde_wasm_bindgen::to_value(&result).unwrap() 128 + } 129 + } 130 + } 131 + 132 + /// Validate a JSON record against an MLF lexicon 133 + #[wasm_bindgen] 134 + pub fn validate_record(lexicon_source: &str, record_json: &str) -> JsValue { 135 + let mut errors = Vec::new(); 136 + 137 + // Parse the lexicon 138 + let lexicon = match mlf_lang::parse_lexicon(lexicon_source) { 139 + Ok(lex) => lex, 140 + Err(e) => { 141 + errors.push(format!("Lexicon parse error: {:?}", e)); 142 + let result = ValidateResult { 143 + success: false, 144 + errors, 145 + }; 146 + return serde_wasm_bindgen::to_value(&result).unwrap(); 147 + } 148 + }; 149 + 150 + // Parse the JSON record 151 + let record: serde_json::Value = match serde_json::from_str(record_json) { 152 + Ok(rec) => rec, 153 + Err(e) => { 154 + errors.push(format!("JSON parse error: {}", e)); 155 + let result = ValidateResult { 156 + success: false, 157 + errors, 158 + }; 159 + return serde_wasm_bindgen::to_value(&result).unwrap(); 160 + } 161 + }; 162 + 163 + // Validate the record 164 + let validator = mlf_validation::RecordValidator::new(&lexicon); 165 + match validator.validate_record(&record) { 166 + Ok(()) => { 167 + let result = ValidateResult { 168 + success: true, 169 + errors: vec![], 170 + }; 171 + serde_wasm_bindgen::to_value(&result).unwrap() 172 + } 173 + Err(validation_errors) => { 174 + for err in validation_errors { 175 + errors.push(err.to_string()); 176 + } 177 + let result = ValidateResult { 178 + success: false, 179 + errors, 180 + }; 181 + serde_wasm_bindgen::to_value(&result).unwrap() 182 + } 183 + } 184 + } 185 + 186 + #[cfg(test)] 187 + mod tests { 188 + use super::*; 189 + 190 + #[test] 191 + fn test_parse_valid() { 192 + let source = r#" 193 + record post { 194 + text: string, 195 + }; 196 + "#; 197 + 198 + let result = parse(source); 199 + // Basic test that it doesn't panic 200 + assert!(!result.is_undefined()); 201 + } 202 + }
+43
resources/prelude.mlf
··· 1 + alias AtIdentifier = string constrained { 2 + format: "at-identifier", 3 + }; 4 + 5 + alias AtUri = string constrained { 6 + format: "at-uri", 7 + }; 8 + 9 + alias Cid = string constrained { 10 + format: "cid", 11 + }; 12 + 13 + alias Datetime = string constrained { 14 + format: "datetime", 15 + }; 16 + 17 + alias Did = string constrained { 18 + format: "did", 19 + }; 20 + 21 + alias Handle = string constrained { 22 + format: "handle", 23 + }; 24 + 25 + alias Nsid = string constrained { 26 + format: "nsid", 27 + }; 28 + 29 + alias Tid = string constrained { 30 + format: "tid", 31 + }; 32 + 33 + alias RecordKey = string constrained { 34 + format: "record-key", 35 + }; 36 + 37 + alias Uri = string constrained { 38 + format: "uri", 39 + }; 40 + 41 + alias Language = string constrained { 42 + format: "language", 43 + };
+8
tree-sitter-mlf/.gitignore
··· 1 + node_modules/ 2 + build/ 3 + *.log 4 + .DS_Store 5 + Cargo.lock 6 + target/ 7 + src/parser.c 8 + src/tree_sitter/
+17
tree-sitter-mlf/Cargo.toml
··· 1 + [package] 2 + name = "tree-sitter-mlf" 3 + version = "0.1.0" 4 + description = "MLF grammar for tree-sitter" 5 + keywords = ["tree-sitter", "parser", "mlf"] 6 + categories = ["parsing", "text-editors"] 7 + license = "MIT" 8 + edition = "2021" 9 + 10 + [lib] 11 + path = "bindings/rust/lib.rs" 12 + 13 + [dependencies] 14 + tree-sitter = "~0.22" 15 + 16 + [build-dependencies] 17 + cc = "1.0"
+95
tree-sitter-mlf/README.md
··· 1 + # tree-sitter-mlf 2 + 3 + MLF (Matt's Lexicon Format) grammar for [tree-sitter](https://tree-sitter.github.io/). 4 + 5 + ## Installation 6 + 7 + ### npm 8 + 9 + ```bash 10 + npm install tree-sitter-mlf 11 + ``` 12 + 13 + ### Cargo 14 + 15 + ```toml 16 + [dependencies] 17 + tree-sitter-mlf = "0.1" 18 + ``` 19 + 20 + ## Usage 21 + 22 + ### Rust 23 + 24 + ```rust 25 + use tree_sitter::Parser; 26 + 27 + let code = r#" 28 + record post { 29 + text: string constrained { 30 + maxLength: 300, 31 + }, 32 + }; 33 + "#; 34 + 35 + let mut parser = Parser::new(); 36 + parser.set_language(&tree_sitter_mlf::language()).unwrap(); 37 + let tree = parser.parse(code, None).unwrap(); 38 + ``` 39 + 40 + ### JavaScript 41 + 42 + ```javascript 43 + const Parser = require('tree-sitter'); 44 + const MLF = require('tree-sitter-mlf'); 45 + 46 + const parser = new Parser(); 47 + parser.setLanguage(MLF); 48 + 49 + const code = ` 50 + record post { 51 + text: string, 52 + }; 53 + `; 54 + 55 + const tree = parser.parse(code); 56 + ``` 57 + 58 + ## Development 59 + 60 + ### Generate the parser 61 + 62 + ```bash 63 + # Install tree-sitter CLI 64 + npm install 65 + 66 + # Generate parser 67 + npx tree-sitter generate 68 + 69 + # Test the grammar 70 + npx tree-sitter test 71 + ``` 72 + 73 + ### Build Rust bindings 74 + 75 + ```bash 76 + cd bindings/rust 77 + cargo build 78 + cargo test 79 + ``` 80 + 81 + ## Features 82 + 83 + - Full MLF syntax support 84 + - Records, aliases, tokens, queries, procedures, subscriptions 85 + - Type system with primitives, references, arrays, unions, objects 86 + - Constrained types with validation rules 87 + - Syntax highlighting queries included 88 + 89 + ## Editor Support 90 + 91 + This grammar can be used with: 92 + - **Neovim** - Via nvim-treesitter 93 + - **Emacs** - Via tree-sitter-mode 94 + - **Helix** - Built-in tree-sitter support 95 + - **VS Code** - Via tree-sitter extensions
+20
tree-sitter-mlf/binding.gyp
··· 1 + { 2 + "targets": [ 3 + { 4 + "target_name": "tree_sitter_mlf_binding", 5 + "dependencies": [ 6 + "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except" 7 + ], 8 + "include_dirs": [ 9 + "src" 10 + ], 11 + "sources": [ 12 + "bindings/node/binding.cc", 13 + "src/parser.c" 14 + ], 15 + "cflags_c": [ 16 + "-std=c11" 17 + ] 18 + } 19 + ] 20 + }
+28
tree-sitter-mlf/bindings/node/binding.cc
··· 1 + #include "tree_sitter/parser.h" 2 + #include <node.h> 3 + #include "nan.h" 4 + 5 + using namespace v8; 6 + 7 + extern "C" TSLanguage * tree_sitter_mlf(); 8 + 9 + namespace { 10 + 11 + NAN_METHOD(New) {} 12 + 13 + void Init(Local<Object> exports, Local<Object> module) { 14 + Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New); 15 + tpl->SetClassName(Nan::New("Language").ToLocalChecked()); 16 + tpl->InstanceTemplate()->SetInternalFieldCount(1); 17 + 18 + Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked(); 19 + Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); 20 + Nan::SetInternalFieldPointer(instance, 0, tree_sitter_mlf()); 21 + 22 + Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("mlf").ToLocalChecked()); 23 + Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); 24 + } 25 + 26 + NODE_MODULE(tree_sitter_mlf_binding, Init) 27 + 28 + } // namespace
+19
tree-sitter-mlf/bindings/node/index.js
··· 1 + try { 2 + module.exports = require("../../build/Release/tree_sitter_mlf_binding"); 3 + } catch (error1) { 4 + if (error1.code !== 'MODULE_NOT_FOUND') { 5 + throw error1; 6 + } 7 + try { 8 + module.exports = require("../../build/Debug/tree_sitter_mlf_binding"); 9 + } catch (error2) { 10 + if (error2.code !== 'MODULE_NOT_FOUND') { 11 + throw error2; 12 + } 13 + throw error1 14 + } 15 + } 16 + 17 + try { 18 + module.exports.nodeTypeInfo = require("../../src/node-types.json"); 19 + } catch (_) {}
+24
tree-sitter-mlf/bindings/rust/build.rs
··· 1 + fn main() { 2 + let src_dir = std::path::Path::new("../../src"); 3 + 4 + let mut c_config = cc::Build::new(); 5 + c_config.include(src_dir); 6 + c_config 7 + .flag_if_supported("-Wno-unused-parameter") 8 + .flag_if_supported("-Wno-unused-but-set-variable") 9 + .flag_if_supported("-Wno-trigraphs"); 10 + let parser_path = src_dir.join("parser.c"); 11 + c_config.file(&parser_path); 12 + 13 + // If your language uses an external scanner written in C, 14 + // then include this block of code: 15 + 16 + /* 17 + let scanner_path = src_dir.join("scanner.c"); 18 + c_config.file(&scanner_path); 19 + println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); 20 + */ 21 + 22 + c_config.compile("tree-sitter-mlf"); 23 + println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); 24 + }
+56
tree-sitter-mlf/bindings/rust/lib.rs
··· 1 + //! This crate provides MLF language support for the [tree-sitter][] parsing library. 2 + //! 3 + //! Typically, you will use the [language][language func] function to add this language to a 4 + //! tree-sitter [Parser][], and then use the parser to parse some code: 5 + //! 6 + //! ``` 7 + //! let code = r#" 8 + //! record post { 9 + //! text: string, 10 + //! }; 11 + //! "#; 12 + //! let mut parser = tree_sitter::Parser::new(); 13 + //! parser.set_language(&tree_sitter_mlf::language()).expect("Error loading MLF grammar"); 14 + //! let tree = parser.parse(code, None).unwrap(); 15 + //! assert!(!tree.root_node().has_error()); 16 + //! ``` 17 + //! 18 + //! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html 19 + //! [language func]: fn.language.html 20 + //! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html 21 + //! [tree-sitter]: https://tree-sitter.github.io/ 22 + 23 + use tree_sitter::Language; 24 + 25 + extern "C" { 26 + fn tree_sitter_mlf() -> Language; 27 + } 28 + 29 + /// Get the tree-sitter [Language][] for this grammar. 30 + /// 31 + /// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html 32 + pub fn language() -> Language { 33 + unsafe { tree_sitter_mlf() } 34 + } 35 + 36 + /// The content of the [`node-types.json`][] file for this grammar. 37 + /// 38 + /// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types 39 + pub const NODE_TYPES: &str = include_str!("../../src/node-types.json"); 40 + 41 + // Uncomment these to include any queries that your grammar contains 42 + // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm"); 43 + // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm"); 44 + // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm"); 45 + // pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm"); 46 + 47 + #[cfg(test)] 48 + mod tests { 49 + #[test] 50 + fn test_can_load_grammar() { 51 + let mut parser = tree_sitter::Parser::new(); 52 + parser 53 + .set_language(&super::language()) 54 + .expect("Error loading MLF language"); 55 + } 56 + }
+259
tree-sitter-mlf/grammar.js
··· 1 + /** 2 + * @file MLF (Matt's Lexicon Format) grammar for tree-sitter 3 + * @author Matt 4 + * @license MIT 5 + */ 6 + 7 + /// <reference types="tree-sitter-cli/dsl" /> 8 + // @ts-check 9 + 10 + module.exports = grammar({ 11 + name: 'mlf', 12 + 13 + extras: $ => [ 14 + /\s/, 15 + $.doc_comment, 16 + $.comment, 17 + ], 18 + 19 + rules: { 20 + source_file: $ => repeat($.item), 21 + 22 + item: $ => choice( 23 + $.namespace_declaration, 24 + $.use_statement, 25 + $.record_definition, 26 + $.alias_definition, 27 + $.token_definition, 28 + $.query_definition, 29 + $.procedure_definition, 30 + $.subscription_definition, 31 + ), 32 + 33 + // Comments 34 + doc_comment: $ => token(seq('///', /.*/)), 35 + comment: $ => token(seq('//', /.*/)), 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 + // Use statements 47 + use_statement: $ => seq( 48 + 'use', 49 + field('path', $.type_path), 50 + ';' 51 + ), 52 + 53 + // Record definition 54 + record_definition: $ => seq( 55 + 'record', 56 + field('name', $.identifier), 57 + field('body', $.record_body), 58 + ';' 59 + ), 60 + 61 + record_body: $ => seq( 62 + '{', 63 + repeat($.field), 64 + '}' 65 + ), 66 + 67 + field: $ => seq( 68 + optional($.doc_comment), 69 + field('name', $.identifier), 70 + optional('?'), 71 + ':', 72 + field('type', $.type), 73 + ',' 74 + ), 75 + 76 + // Alias definition 77 + alias_definition: $ => seq( 78 + 'alias', 79 + field('name', $.identifier), 80 + '=', 81 + field('type', $.type), 82 + ';' 83 + ), 84 + 85 + // Token definition 86 + token_definition: $ => seq( 87 + 'token', 88 + field('name', $.identifier), 89 + ';' 90 + ), 91 + 92 + // Query definition 93 + query_definition: $ => seq( 94 + 'query', 95 + field('name', $.identifier), 96 + field('params', $.parameter_list), 97 + field('return', $.return_type), 98 + ';' 99 + ), 100 + 101 + // Procedure definition 102 + procedure_definition: $ => seq( 103 + 'procedure', 104 + field('name', $.identifier), 105 + field('params', $.parameter_list), 106 + field('return', $.return_type), 107 + ';' 108 + ), 109 + 110 + // Subscription definition 111 + subscription_definition: $ => seq( 112 + 'subscription', 113 + field('name', $.identifier), 114 + field('params', $.parameter_list), 115 + '->', 116 + field('messages', $.type), 117 + ';' 118 + ), 119 + 120 + parameter_list: $ => seq( 121 + '(', 122 + optional(seq( 123 + $.parameter, 124 + repeat(seq(',', $.parameter)), 125 + optional(',') 126 + )), 127 + ')' 128 + ), 129 + 130 + parameter: $ => seq( 131 + field('name', $.identifier), 132 + optional('?'), 133 + ':', 134 + field('type', $.type) 135 + ), 136 + 137 + return_type: $ => choice( 138 + seq('->', $.type), 139 + seq( 140 + '->', 141 + $.type, 142 + 'throws', 143 + '{', 144 + repeat($.error_definition), 145 + '}' 146 + ) 147 + ), 148 + 149 + error_definition: $ => seq( 150 + optional($.doc_comment), 151 + field('name', $.identifier), 152 + ',' 153 + ), 154 + 155 + // Types 156 + type: $ => choice( 157 + $.union_type, 158 + $.non_union_type, 159 + ), 160 + 161 + non_union_type: $ => choice( 162 + $.primitive_type, 163 + $.reference_type, 164 + $.array_type, 165 + $.object_type, 166 + $.constrained_type, 167 + ), 168 + 169 + primitive_type: $ => choice( 170 + 'null', 171 + 'boolean', 172 + 'integer', 173 + 'number', 174 + 'string', 175 + 'bytes', 176 + 'blob', 177 + 'unknown' 178 + ), 179 + 180 + reference_type: $ => $.type_path, 181 + 182 + type_path: $ => seq( 183 + $.identifier, 184 + repeat(seq('.', $.identifier)) 185 + ), 186 + 187 + array_type: $ => seq( 188 + '[', 189 + field('element', $.type), 190 + ']' 191 + ), 192 + 193 + union_type: $ => prec.left(seq( 194 + $.non_union_type, 195 + repeat1(seq('|', $.non_union_type)) 196 + )), 197 + 198 + object_type: $ => seq( 199 + '{', 200 + repeat($.field), 201 + '}' 202 + ), 203 + 204 + constrained_type: $ => seq( 205 + field('base', $.non_union_type), 206 + 'constrained', 207 + field('constraints', $.constraint_block) 208 + ), 209 + 210 + constraint_block: $ => seq( 211 + '{', 212 + repeat($.constraint), 213 + '}' 214 + ), 215 + 216 + constraint: $ => seq( 217 + field('name', $.identifier), 218 + ':', 219 + field('value', $.constraint_value), 220 + ',' 221 + ), 222 + 223 + constraint_value: $ => choice( 224 + $.number, 225 + $.string, 226 + $.boolean, 227 + $.array_literal, 228 + ), 229 + 230 + array_literal: $ => seq( 231 + '[', 232 + optional(seq( 233 + $.constraint_value, 234 + repeat(seq(',', $.constraint_value)), 235 + optional(',') 236 + )), 237 + ']' 238 + ), 239 + 240 + // Literals 241 + identifier: $ => choice( 242 + /[a-zA-Z_][a-zA-Z0-9_]*/, 243 + seq('`', /[^`]+/, '`') 244 + ), 245 + 246 + number: $ => /-?\d+(\.\d+)?/, 247 + 248 + string: $ => token(seq( 249 + '"', 250 + repeat(choice( 251 + /[^"\\\n]/, 252 + /\\./ 253 + )), 254 + '"' 255 + )), 256 + 257 + boolean: $ => choice('true', 'false'), 258 + } 259 + });
+40
tree-sitter-mlf/package.json
··· 1 + { 2 + "name": "tree-sitter-mlf", 3 + "version": "0.1.0", 4 + "description": "MLF (Matt's Lexicon Format) grammar for tree-sitter", 5 + "main": "bindings/node", 6 + "types": "bindings/node", 7 + "keywords": [ 8 + "tree-sitter", 9 + "parser", 10 + "mlf", 11 + "lexicon", 12 + "atproto" 13 + ], 14 + "author": "Matt", 15 + "license": "MIT", 16 + "dependencies": { 17 + "node-addon-api": "^7.1.0", 18 + "node-gyp-build": "^4.8.0" 19 + }, 20 + "devDependencies": { 21 + "tree-sitter-cli": "^0.22.6", 22 + "prebuildify": "^6.0.0" 23 + }, 24 + "peerDependencies": { 25 + "tree-sitter": "^0.21.0" 26 + }, 27 + "peerDependenciesMeta": { 28 + "tree-sitter": { 29 + "optional": true 30 + } 31 + }, 32 + "tree-sitter": [ 33 + { 34 + "scope": "source.mlf", 35 + "file-types": [ 36 + "mlf" 37 + ] 38 + } 39 + ] 40 + }
+97
tree-sitter-mlf/queries/highlights.scm
··· 1 + ; Keywords 2 + [ 3 + "namespace" 4 + "use" 5 + "record" 6 + "alias" 7 + "token" 8 + "query" 9 + "procedure" 10 + "subscription" 11 + "throws" 12 + "constrained" 13 + ] @keyword 14 + 15 + ; Primitive types 16 + [ 17 + "null" 18 + "boolean" 19 + "integer" 20 + "number" 21 + "string" 22 + "bytes" 23 + "blob" 24 + "unknown" 25 + ] @type.builtin 26 + 27 + ; Type references 28 + (reference_type 29 + (type_path) @type) 30 + 31 + ; Function/method names 32 + (query_definition 33 + name: (identifier) @function) 34 + 35 + (procedure_definition 36 + name: (identifier) @function) 37 + 38 + (subscription_definition 39 + name: (identifier) @function) 40 + 41 + ; Record names 42 + (record_definition 43 + name: (identifier) @type) 44 + 45 + ; Alias names 46 + (alias_definition 47 + name: (identifier) @type) 48 + 49 + ; Token names 50 + (token_definition 51 + name: (identifier) @constant) 52 + 53 + ; Field names 54 + (field 55 + name: (identifier) @property) 56 + 57 + ; Parameter names 58 + (parameter 59 + name: (identifier) @variable.parameter) 60 + 61 + ; Error names 62 + (error_definition 63 + name: (identifier) @constant) 64 + 65 + ; Literals 66 + (string) @string 67 + (number) @number 68 + (boolean) @constant.builtin 69 + 70 + ; Comments 71 + (doc_comment) @comment.documentation 72 + (comment) @comment 73 + 74 + ; Operators 75 + [ 76 + ":" 77 + "=" 78 + "->" 79 + "|" 80 + "?" 81 + ] @operator 82 + 83 + ; Delimiters 84 + [ 85 + "(" 86 + ")" 87 + "[" 88 + "]" 89 + "{" 90 + "}" 91 + ] @punctuation.bracket 92 + 93 + [ 94 + "," 95 + ";" 96 + "." 97 + ] @punctuation.delimiter
+994
tree-sitter-mlf/src/grammar.json
··· 1 + { 2 + "name": "mlf", 3 + "rules": { 4 + "source_file": { 5 + "type": "REPEAT", 6 + "content": { 7 + "type": "SYMBOL", 8 + "name": "item" 9 + } 10 + }, 11 + "item": { 12 + "type": "CHOICE", 13 + "members": [ 14 + { 15 + "type": "SYMBOL", 16 + "name": "namespace_declaration" 17 + }, 18 + { 19 + "type": "SYMBOL", 20 + "name": "use_statement" 21 + }, 22 + { 23 + "type": "SYMBOL", 24 + "name": "record_definition" 25 + }, 26 + { 27 + "type": "SYMBOL", 28 + "name": "alias_definition" 29 + }, 30 + { 31 + "type": "SYMBOL", 32 + "name": "token_definition" 33 + }, 34 + { 35 + "type": "SYMBOL", 36 + "name": "query_definition" 37 + }, 38 + { 39 + "type": "SYMBOL", 40 + "name": "procedure_definition" 41 + }, 42 + { 43 + "type": "SYMBOL", 44 + "name": "subscription_definition" 45 + } 46 + ] 47 + }, 48 + "doc_comment": { 49 + "type": "TOKEN", 50 + "content": { 51 + "type": "SEQ", 52 + "members": [ 53 + { 54 + "type": "STRING", 55 + "value": "///" 56 + }, 57 + { 58 + "type": "PATTERN", 59 + "value": ".*" 60 + } 61 + ] 62 + } 63 + }, 64 + "comment": { 65 + "type": "TOKEN", 66 + "content": { 67 + "type": "SEQ", 68 + "members": [ 69 + { 70 + "type": "STRING", 71 + "value": "//" 72 + }, 73 + { 74 + "type": "PATTERN", 75 + "value": ".*" 76 + } 77 + ] 78 + } 79 + }, 80 + "namespace_declaration": { 81 + "type": "SEQ", 82 + "members": [ 83 + { 84 + "type": "STRING", 85 + "value": "namespace" 86 + }, 87 + { 88 + "type": "FIELD", 89 + "name": "name", 90 + "content": { 91 + "type": "SYMBOL", 92 + "name": "namespace_identifier" 93 + } 94 + }, 95 + { 96 + "type": "STRING", 97 + "value": ";" 98 + } 99 + ] 100 + }, 101 + "namespace_identifier": { 102 + "type": "PATTERN", 103 + "value": "[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*" 104 + }, 105 + "use_statement": { 106 + "type": "SEQ", 107 + "members": [ 108 + { 109 + "type": "STRING", 110 + "value": "use" 111 + }, 112 + { 113 + "type": "FIELD", 114 + "name": "path", 115 + "content": { 116 + "type": "SYMBOL", 117 + "name": "type_path" 118 + } 119 + }, 120 + { 121 + "type": "STRING", 122 + "value": ";" 123 + } 124 + ] 125 + }, 126 + "record_definition": { 127 + "type": "SEQ", 128 + "members": [ 129 + { 130 + "type": "STRING", 131 + "value": "record" 132 + }, 133 + { 134 + "type": "FIELD", 135 + "name": "name", 136 + "content": { 137 + "type": "SYMBOL", 138 + "name": "identifier" 139 + } 140 + }, 141 + { 142 + "type": "FIELD", 143 + "name": "body", 144 + "content": { 145 + "type": "SYMBOL", 146 + "name": "record_body" 147 + } 148 + }, 149 + { 150 + "type": "STRING", 151 + "value": ";" 152 + } 153 + ] 154 + }, 155 + "record_body": { 156 + "type": "SEQ", 157 + "members": [ 158 + { 159 + "type": "STRING", 160 + "value": "{" 161 + }, 162 + { 163 + "type": "REPEAT", 164 + "content": { 165 + "type": "SYMBOL", 166 + "name": "field" 167 + } 168 + }, 169 + { 170 + "type": "STRING", 171 + "value": "}" 172 + } 173 + ] 174 + }, 175 + "field": { 176 + "type": "SEQ", 177 + "members": [ 178 + { 179 + "type": "CHOICE", 180 + "members": [ 181 + { 182 + "type": "SYMBOL", 183 + "name": "doc_comment" 184 + }, 185 + { 186 + "type": "BLANK" 187 + } 188 + ] 189 + }, 190 + { 191 + "type": "FIELD", 192 + "name": "name", 193 + "content": { 194 + "type": "SYMBOL", 195 + "name": "identifier" 196 + } 197 + }, 198 + { 199 + "type": "CHOICE", 200 + "members": [ 201 + { 202 + "type": "STRING", 203 + "value": "?" 204 + }, 205 + { 206 + "type": "BLANK" 207 + } 208 + ] 209 + }, 210 + { 211 + "type": "STRING", 212 + "value": ":" 213 + }, 214 + { 215 + "type": "FIELD", 216 + "name": "type", 217 + "content": { 218 + "type": "SYMBOL", 219 + "name": "type" 220 + } 221 + }, 222 + { 223 + "type": "STRING", 224 + "value": "," 225 + } 226 + ] 227 + }, 228 + "alias_definition": { 229 + "type": "SEQ", 230 + "members": [ 231 + { 232 + "type": "STRING", 233 + "value": "alias" 234 + }, 235 + { 236 + "type": "FIELD", 237 + "name": "name", 238 + "content": { 239 + "type": "SYMBOL", 240 + "name": "identifier" 241 + } 242 + }, 243 + { 244 + "type": "STRING", 245 + "value": "=" 246 + }, 247 + { 248 + "type": "FIELD", 249 + "name": "type", 250 + "content": { 251 + "type": "SYMBOL", 252 + "name": "type" 253 + } 254 + }, 255 + { 256 + "type": "STRING", 257 + "value": ";" 258 + } 259 + ] 260 + }, 261 + "token_definition": { 262 + "type": "SEQ", 263 + "members": [ 264 + { 265 + "type": "STRING", 266 + "value": "token" 267 + }, 268 + { 269 + "type": "FIELD", 270 + "name": "name", 271 + "content": { 272 + "type": "SYMBOL", 273 + "name": "identifier" 274 + } 275 + }, 276 + { 277 + "type": "STRING", 278 + "value": ";" 279 + } 280 + ] 281 + }, 282 + "query_definition": { 283 + "type": "SEQ", 284 + "members": [ 285 + { 286 + "type": "STRING", 287 + "value": "query" 288 + }, 289 + { 290 + "type": "FIELD", 291 + "name": "name", 292 + "content": { 293 + "type": "SYMBOL", 294 + "name": "identifier" 295 + } 296 + }, 297 + { 298 + "type": "FIELD", 299 + "name": "params", 300 + "content": { 301 + "type": "SYMBOL", 302 + "name": "parameter_list" 303 + } 304 + }, 305 + { 306 + "type": "FIELD", 307 + "name": "return", 308 + "content": { 309 + "type": "SYMBOL", 310 + "name": "return_type" 311 + } 312 + }, 313 + { 314 + "type": "STRING", 315 + "value": ";" 316 + } 317 + ] 318 + }, 319 + "procedure_definition": { 320 + "type": "SEQ", 321 + "members": [ 322 + { 323 + "type": "STRING", 324 + "value": "procedure" 325 + }, 326 + { 327 + "type": "FIELD", 328 + "name": "name", 329 + "content": { 330 + "type": "SYMBOL", 331 + "name": "identifier" 332 + } 333 + }, 334 + { 335 + "type": "FIELD", 336 + "name": "params", 337 + "content": { 338 + "type": "SYMBOL", 339 + "name": "parameter_list" 340 + } 341 + }, 342 + { 343 + "type": "FIELD", 344 + "name": "return", 345 + "content": { 346 + "type": "SYMBOL", 347 + "name": "return_type" 348 + } 349 + }, 350 + { 351 + "type": "STRING", 352 + "value": ";" 353 + } 354 + ] 355 + }, 356 + "subscription_definition": { 357 + "type": "SEQ", 358 + "members": [ 359 + { 360 + "type": "STRING", 361 + "value": "subscription" 362 + }, 363 + { 364 + "type": "FIELD", 365 + "name": "name", 366 + "content": { 367 + "type": "SYMBOL", 368 + "name": "identifier" 369 + } 370 + }, 371 + { 372 + "type": "FIELD", 373 + "name": "params", 374 + "content": { 375 + "type": "SYMBOL", 376 + "name": "parameter_list" 377 + } 378 + }, 379 + { 380 + "type": "STRING", 381 + "value": "->" 382 + }, 383 + { 384 + "type": "FIELD", 385 + "name": "messages", 386 + "content": { 387 + "type": "SYMBOL", 388 + "name": "type" 389 + } 390 + }, 391 + { 392 + "type": "STRING", 393 + "value": ";" 394 + } 395 + ] 396 + }, 397 + "parameter_list": { 398 + "type": "SEQ", 399 + "members": [ 400 + { 401 + "type": "STRING", 402 + "value": "(" 403 + }, 404 + { 405 + "type": "CHOICE", 406 + "members": [ 407 + { 408 + "type": "SEQ", 409 + "members": [ 410 + { 411 + "type": "SYMBOL", 412 + "name": "parameter" 413 + }, 414 + { 415 + "type": "REPEAT", 416 + "content": { 417 + "type": "SEQ", 418 + "members": [ 419 + { 420 + "type": "STRING", 421 + "value": "," 422 + }, 423 + { 424 + "type": "SYMBOL", 425 + "name": "parameter" 426 + } 427 + ] 428 + } 429 + }, 430 + { 431 + "type": "CHOICE", 432 + "members": [ 433 + { 434 + "type": "STRING", 435 + "value": "," 436 + }, 437 + { 438 + "type": "BLANK" 439 + } 440 + ] 441 + } 442 + ] 443 + }, 444 + { 445 + "type": "BLANK" 446 + } 447 + ] 448 + }, 449 + { 450 + "type": "STRING", 451 + "value": ")" 452 + } 453 + ] 454 + }, 455 + "parameter": { 456 + "type": "SEQ", 457 + "members": [ 458 + { 459 + "type": "FIELD", 460 + "name": "name", 461 + "content": { 462 + "type": "SYMBOL", 463 + "name": "identifier" 464 + } 465 + }, 466 + { 467 + "type": "CHOICE", 468 + "members": [ 469 + { 470 + "type": "STRING", 471 + "value": "?" 472 + }, 473 + { 474 + "type": "BLANK" 475 + } 476 + ] 477 + }, 478 + { 479 + "type": "STRING", 480 + "value": ":" 481 + }, 482 + { 483 + "type": "FIELD", 484 + "name": "type", 485 + "content": { 486 + "type": "SYMBOL", 487 + "name": "type" 488 + } 489 + } 490 + ] 491 + }, 492 + "return_type": { 493 + "type": "CHOICE", 494 + "members": [ 495 + { 496 + "type": "SEQ", 497 + "members": [ 498 + { 499 + "type": "STRING", 500 + "value": "->" 501 + }, 502 + { 503 + "type": "SYMBOL", 504 + "name": "type" 505 + } 506 + ] 507 + }, 508 + { 509 + "type": "SEQ", 510 + "members": [ 511 + { 512 + "type": "STRING", 513 + "value": "->" 514 + }, 515 + { 516 + "type": "SYMBOL", 517 + "name": "type" 518 + }, 519 + { 520 + "type": "STRING", 521 + "value": "throws" 522 + }, 523 + { 524 + "type": "STRING", 525 + "value": "{" 526 + }, 527 + { 528 + "type": "REPEAT", 529 + "content": { 530 + "type": "SYMBOL", 531 + "name": "error_definition" 532 + } 533 + }, 534 + { 535 + "type": "STRING", 536 + "value": "}" 537 + } 538 + ] 539 + } 540 + ] 541 + }, 542 + "error_definition": { 543 + "type": "SEQ", 544 + "members": [ 545 + { 546 + "type": "CHOICE", 547 + "members": [ 548 + { 549 + "type": "SYMBOL", 550 + "name": "doc_comment" 551 + }, 552 + { 553 + "type": "BLANK" 554 + } 555 + ] 556 + }, 557 + { 558 + "type": "FIELD", 559 + "name": "name", 560 + "content": { 561 + "type": "SYMBOL", 562 + "name": "identifier" 563 + } 564 + }, 565 + { 566 + "type": "STRING", 567 + "value": "," 568 + } 569 + ] 570 + }, 571 + "type": { 572 + "type": "CHOICE", 573 + "members": [ 574 + { 575 + "type": "SYMBOL", 576 + "name": "union_type" 577 + }, 578 + { 579 + "type": "SYMBOL", 580 + "name": "non_union_type" 581 + } 582 + ] 583 + }, 584 + "non_union_type": { 585 + "type": "CHOICE", 586 + "members": [ 587 + { 588 + "type": "SYMBOL", 589 + "name": "primitive_type" 590 + }, 591 + { 592 + "type": "SYMBOL", 593 + "name": "reference_type" 594 + }, 595 + { 596 + "type": "SYMBOL", 597 + "name": "array_type" 598 + }, 599 + { 600 + "type": "SYMBOL", 601 + "name": "object_type" 602 + }, 603 + { 604 + "type": "SYMBOL", 605 + "name": "constrained_type" 606 + } 607 + ] 608 + }, 609 + "primitive_type": { 610 + "type": "CHOICE", 611 + "members": [ 612 + { 613 + "type": "STRING", 614 + "value": "null" 615 + }, 616 + { 617 + "type": "STRING", 618 + "value": "boolean" 619 + }, 620 + { 621 + "type": "STRING", 622 + "value": "integer" 623 + }, 624 + { 625 + "type": "STRING", 626 + "value": "number" 627 + }, 628 + { 629 + "type": "STRING", 630 + "value": "string" 631 + }, 632 + { 633 + "type": "STRING", 634 + "value": "bytes" 635 + }, 636 + { 637 + "type": "STRING", 638 + "value": "blob" 639 + }, 640 + { 641 + "type": "STRING", 642 + "value": "unknown" 643 + } 644 + ] 645 + }, 646 + "reference_type": { 647 + "type": "SYMBOL", 648 + "name": "type_path" 649 + }, 650 + "type_path": { 651 + "type": "SEQ", 652 + "members": [ 653 + { 654 + "type": "SYMBOL", 655 + "name": "identifier" 656 + }, 657 + { 658 + "type": "REPEAT", 659 + "content": { 660 + "type": "SEQ", 661 + "members": [ 662 + { 663 + "type": "STRING", 664 + "value": "." 665 + }, 666 + { 667 + "type": "SYMBOL", 668 + "name": "identifier" 669 + } 670 + ] 671 + } 672 + } 673 + ] 674 + }, 675 + "array_type": { 676 + "type": "SEQ", 677 + "members": [ 678 + { 679 + "type": "STRING", 680 + "value": "[" 681 + }, 682 + { 683 + "type": "FIELD", 684 + "name": "element", 685 + "content": { 686 + "type": "SYMBOL", 687 + "name": "type" 688 + } 689 + }, 690 + { 691 + "type": "STRING", 692 + "value": "]" 693 + } 694 + ] 695 + }, 696 + "union_type": { 697 + "type": "PREC_LEFT", 698 + "value": 0, 699 + "content": { 700 + "type": "SEQ", 701 + "members": [ 702 + { 703 + "type": "SYMBOL", 704 + "name": "non_union_type" 705 + }, 706 + { 707 + "type": "REPEAT1", 708 + "content": { 709 + "type": "SEQ", 710 + "members": [ 711 + { 712 + "type": "STRING", 713 + "value": "|" 714 + }, 715 + { 716 + "type": "SYMBOL", 717 + "name": "non_union_type" 718 + } 719 + ] 720 + } 721 + } 722 + ] 723 + } 724 + }, 725 + "object_type": { 726 + "type": "SEQ", 727 + "members": [ 728 + { 729 + "type": "STRING", 730 + "value": "{" 731 + }, 732 + { 733 + "type": "REPEAT", 734 + "content": { 735 + "type": "SYMBOL", 736 + "name": "field" 737 + } 738 + }, 739 + { 740 + "type": "STRING", 741 + "value": "}" 742 + } 743 + ] 744 + }, 745 + "constrained_type": { 746 + "type": "SEQ", 747 + "members": [ 748 + { 749 + "type": "FIELD", 750 + "name": "base", 751 + "content": { 752 + "type": "SYMBOL", 753 + "name": "non_union_type" 754 + } 755 + }, 756 + { 757 + "type": "STRING", 758 + "value": "constrained" 759 + }, 760 + { 761 + "type": "FIELD", 762 + "name": "constraints", 763 + "content": { 764 + "type": "SYMBOL", 765 + "name": "constraint_block" 766 + } 767 + } 768 + ] 769 + }, 770 + "constraint_block": { 771 + "type": "SEQ", 772 + "members": [ 773 + { 774 + "type": "STRING", 775 + "value": "{" 776 + }, 777 + { 778 + "type": "REPEAT", 779 + "content": { 780 + "type": "SYMBOL", 781 + "name": "constraint" 782 + } 783 + }, 784 + { 785 + "type": "STRING", 786 + "value": "}" 787 + } 788 + ] 789 + }, 790 + "constraint": { 791 + "type": "SEQ", 792 + "members": [ 793 + { 794 + "type": "FIELD", 795 + "name": "name", 796 + "content": { 797 + "type": "SYMBOL", 798 + "name": "identifier" 799 + } 800 + }, 801 + { 802 + "type": "STRING", 803 + "value": ":" 804 + }, 805 + { 806 + "type": "FIELD", 807 + "name": "value", 808 + "content": { 809 + "type": "SYMBOL", 810 + "name": "constraint_value" 811 + } 812 + }, 813 + { 814 + "type": "STRING", 815 + "value": "," 816 + } 817 + ] 818 + }, 819 + "constraint_value": { 820 + "type": "CHOICE", 821 + "members": [ 822 + { 823 + "type": "SYMBOL", 824 + "name": "number" 825 + }, 826 + { 827 + "type": "SYMBOL", 828 + "name": "string" 829 + }, 830 + { 831 + "type": "SYMBOL", 832 + "name": "boolean" 833 + }, 834 + { 835 + "type": "SYMBOL", 836 + "name": "array_literal" 837 + } 838 + ] 839 + }, 840 + "array_literal": { 841 + "type": "SEQ", 842 + "members": [ 843 + { 844 + "type": "STRING", 845 + "value": "[" 846 + }, 847 + { 848 + "type": "CHOICE", 849 + "members": [ 850 + { 851 + "type": "SEQ", 852 + "members": [ 853 + { 854 + "type": "SYMBOL", 855 + "name": "constraint_value" 856 + }, 857 + { 858 + "type": "REPEAT", 859 + "content": { 860 + "type": "SEQ", 861 + "members": [ 862 + { 863 + "type": "STRING", 864 + "value": "," 865 + }, 866 + { 867 + "type": "SYMBOL", 868 + "name": "constraint_value" 869 + } 870 + ] 871 + } 872 + }, 873 + { 874 + "type": "CHOICE", 875 + "members": [ 876 + { 877 + "type": "STRING", 878 + "value": "," 879 + }, 880 + { 881 + "type": "BLANK" 882 + } 883 + ] 884 + } 885 + ] 886 + }, 887 + { 888 + "type": "BLANK" 889 + } 890 + ] 891 + }, 892 + { 893 + "type": "STRING", 894 + "value": "]" 895 + } 896 + ] 897 + }, 898 + "identifier": { 899 + "type": "CHOICE", 900 + "members": [ 901 + { 902 + "type": "PATTERN", 903 + "value": "[a-zA-Z_][a-zA-Z0-9_]*" 904 + }, 905 + { 906 + "type": "SEQ", 907 + "members": [ 908 + { 909 + "type": "STRING", 910 + "value": "`" 911 + }, 912 + { 913 + "type": "PATTERN", 914 + "value": "[^`]+" 915 + }, 916 + { 917 + "type": "STRING", 918 + "value": "`" 919 + } 920 + ] 921 + } 922 + ] 923 + }, 924 + "number": { 925 + "type": "PATTERN", 926 + "value": "-?\\d+(\\.\\d+)?" 927 + }, 928 + "string": { 929 + "type": "TOKEN", 930 + "content": { 931 + "type": "SEQ", 932 + "members": [ 933 + { 934 + "type": "STRING", 935 + "value": "\"" 936 + }, 937 + { 938 + "type": "REPEAT", 939 + "content": { 940 + "type": "CHOICE", 941 + "members": [ 942 + { 943 + "type": "PATTERN", 944 + "value": "[^\"\\\\\\n]" 945 + }, 946 + { 947 + "type": "PATTERN", 948 + "value": "\\\\." 949 + } 950 + ] 951 + } 952 + }, 953 + { 954 + "type": "STRING", 955 + "value": "\"" 956 + } 957 + ] 958 + } 959 + }, 960 + "boolean": { 961 + "type": "CHOICE", 962 + "members": [ 963 + { 964 + "type": "STRING", 965 + "value": "true" 966 + }, 967 + { 968 + "type": "STRING", 969 + "value": "false" 970 + } 971 + ] 972 + } 973 + }, 974 + "extras": [ 975 + { 976 + "type": "PATTERN", 977 + "value": "\\s" 978 + }, 979 + { 980 + "type": "SYMBOL", 981 + "name": "doc_comment" 982 + }, 983 + { 984 + "type": "SYMBOL", 985 + "name": "comment" 986 + } 987 + ], 988 + "conflicts": [], 989 + "precedences": [], 990 + "externals": [], 991 + "inline": [], 992 + "supertypes": [] 993 + } 994 +
+815
tree-sitter-mlf/src/node-types.json
··· 1 + [ 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 + "type": "array_literal", 30 + "named": true, 31 + "fields": {}, 32 + "children": { 33 + "multiple": true, 34 + "required": false, 35 + "types": [ 36 + { 37 + "type": "constraint_value", 38 + "named": true 39 + } 40 + ] 41 + } 42 + }, 43 + { 44 + "type": "array_type", 45 + "named": true, 46 + "fields": { 47 + "element": { 48 + "multiple": false, 49 + "required": true, 50 + "types": [ 51 + { 52 + "type": "type", 53 + "named": true 54 + } 55 + ] 56 + } 57 + } 58 + }, 59 + { 60 + "type": "boolean", 61 + "named": true, 62 + "fields": {} 63 + }, 64 + { 65 + "type": "constrained_type", 66 + "named": true, 67 + "fields": { 68 + "base": { 69 + "multiple": false, 70 + "required": true, 71 + "types": [ 72 + { 73 + "type": "non_union_type", 74 + "named": true 75 + } 76 + ] 77 + }, 78 + "constraints": { 79 + "multiple": false, 80 + "required": true, 81 + "types": [ 82 + { 83 + "type": "constraint_block", 84 + "named": true 85 + } 86 + ] 87 + } 88 + } 89 + }, 90 + { 91 + "type": "constraint", 92 + "named": true, 93 + "fields": { 94 + "name": { 95 + "multiple": false, 96 + "required": true, 97 + "types": [ 98 + { 99 + "type": "identifier", 100 + "named": true 101 + } 102 + ] 103 + }, 104 + "value": { 105 + "multiple": false, 106 + "required": true, 107 + "types": [ 108 + { 109 + "type": "constraint_value", 110 + "named": true 111 + } 112 + ] 113 + } 114 + } 115 + }, 116 + { 117 + "type": "constraint_block", 118 + "named": true, 119 + "fields": {}, 120 + "children": { 121 + "multiple": true, 122 + "required": false, 123 + "types": [ 124 + { 125 + "type": "constraint", 126 + "named": true 127 + } 128 + ] 129 + } 130 + }, 131 + { 132 + "type": "constraint_value", 133 + "named": true, 134 + "fields": {}, 135 + "children": { 136 + "multiple": false, 137 + "required": true, 138 + "types": [ 139 + { 140 + "type": "array_literal", 141 + "named": true 142 + }, 143 + { 144 + "type": "boolean", 145 + "named": true 146 + }, 147 + { 148 + "type": "number", 149 + "named": true 150 + }, 151 + { 152 + "type": "string", 153 + "named": true 154 + } 155 + ] 156 + } 157 + }, 158 + { 159 + "type": "error_definition", 160 + "named": true, 161 + "fields": { 162 + "name": { 163 + "multiple": false, 164 + "required": true, 165 + "types": [ 166 + { 167 + "type": "identifier", 168 + "named": true 169 + } 170 + ] 171 + } 172 + }, 173 + "children": { 174 + "multiple": false, 175 + "required": false, 176 + "types": [ 177 + { 178 + "type": "doc_comment", 179 + "named": true 180 + } 181 + ] 182 + } 183 + }, 184 + { 185 + "type": "field", 186 + "named": true, 187 + "fields": { 188 + "name": { 189 + "multiple": false, 190 + "required": true, 191 + "types": [ 192 + { 193 + "type": "identifier", 194 + "named": true 195 + } 196 + ] 197 + }, 198 + "type": { 199 + "multiple": false, 200 + "required": true, 201 + "types": [ 202 + { 203 + "type": "type", 204 + "named": true 205 + } 206 + ] 207 + } 208 + }, 209 + "children": { 210 + "multiple": false, 211 + "required": false, 212 + "types": [ 213 + { 214 + "type": "doc_comment", 215 + "named": true 216 + } 217 + ] 218 + } 219 + }, 220 + { 221 + "type": "identifier", 222 + "named": true, 223 + "fields": {} 224 + }, 225 + { 226 + "type": "item", 227 + "named": true, 228 + "fields": {}, 229 + "children": { 230 + "multiple": false, 231 + "required": true, 232 + "types": [ 233 + { 234 + "type": "alias_definition", 235 + "named": true 236 + }, 237 + { 238 + "type": "namespace_declaration", 239 + "named": true 240 + }, 241 + { 242 + "type": "procedure_definition", 243 + "named": true 244 + }, 245 + { 246 + "type": "query_definition", 247 + "named": true 248 + }, 249 + { 250 + "type": "record_definition", 251 + "named": true 252 + }, 253 + { 254 + "type": "subscription_definition", 255 + "named": true 256 + }, 257 + { 258 + "type": "token_definition", 259 + "named": true 260 + }, 261 + { 262 + "type": "use_statement", 263 + "named": true 264 + } 265 + ] 266 + } 267 + }, 268 + { 269 + "type": "namespace_declaration", 270 + "named": true, 271 + "fields": { 272 + "name": { 273 + "multiple": false, 274 + "required": true, 275 + "types": [ 276 + { 277 + "type": "namespace_identifier", 278 + "named": true 279 + } 280 + ] 281 + } 282 + } 283 + }, 284 + { 285 + "type": "non_union_type", 286 + "named": true, 287 + "fields": {}, 288 + "children": { 289 + "multiple": false, 290 + "required": true, 291 + "types": [ 292 + { 293 + "type": "array_type", 294 + "named": true 295 + }, 296 + { 297 + "type": "constrained_type", 298 + "named": true 299 + }, 300 + { 301 + "type": "object_type", 302 + "named": true 303 + }, 304 + { 305 + "type": "primitive_type", 306 + "named": true 307 + }, 308 + { 309 + "type": "reference_type", 310 + "named": true 311 + } 312 + ] 313 + } 314 + }, 315 + { 316 + "type": "object_type", 317 + "named": true, 318 + "fields": {}, 319 + "children": { 320 + "multiple": true, 321 + "required": false, 322 + "types": [ 323 + { 324 + "type": "field", 325 + "named": true 326 + } 327 + ] 328 + } 329 + }, 330 + { 331 + "type": "parameter", 332 + "named": true, 333 + "fields": { 334 + "name": { 335 + "multiple": false, 336 + "required": true, 337 + "types": [ 338 + { 339 + "type": "identifier", 340 + "named": true 341 + } 342 + ] 343 + }, 344 + "type": { 345 + "multiple": false, 346 + "required": true, 347 + "types": [ 348 + { 349 + "type": "type", 350 + "named": true 351 + } 352 + ] 353 + } 354 + } 355 + }, 356 + { 357 + "type": "parameter_list", 358 + "named": true, 359 + "fields": {}, 360 + "children": { 361 + "multiple": true, 362 + "required": false, 363 + "types": [ 364 + { 365 + "type": "parameter", 366 + "named": true 367 + } 368 + ] 369 + } 370 + }, 371 + { 372 + "type": "primitive_type", 373 + "named": true, 374 + "fields": {} 375 + }, 376 + { 377 + "type": "procedure_definition", 378 + "named": true, 379 + "fields": { 380 + "name": { 381 + "multiple": false, 382 + "required": true, 383 + "types": [ 384 + { 385 + "type": "identifier", 386 + "named": true 387 + } 388 + ] 389 + }, 390 + "params": { 391 + "multiple": false, 392 + "required": true, 393 + "types": [ 394 + { 395 + "type": "parameter_list", 396 + "named": true 397 + } 398 + ] 399 + }, 400 + "return": { 401 + "multiple": false, 402 + "required": true, 403 + "types": [ 404 + { 405 + "type": "return_type", 406 + "named": true 407 + } 408 + ] 409 + } 410 + } 411 + }, 412 + { 413 + "type": "query_definition", 414 + "named": true, 415 + "fields": { 416 + "name": { 417 + "multiple": false, 418 + "required": true, 419 + "types": [ 420 + { 421 + "type": "identifier", 422 + "named": true 423 + } 424 + ] 425 + }, 426 + "params": { 427 + "multiple": false, 428 + "required": true, 429 + "types": [ 430 + { 431 + "type": "parameter_list", 432 + "named": true 433 + } 434 + ] 435 + }, 436 + "return": { 437 + "multiple": false, 438 + "required": true, 439 + "types": [ 440 + { 441 + "type": "return_type", 442 + "named": true 443 + } 444 + ] 445 + } 446 + } 447 + }, 448 + { 449 + "type": "record_body", 450 + "named": true, 451 + "fields": {}, 452 + "children": { 453 + "multiple": true, 454 + "required": false, 455 + "types": [ 456 + { 457 + "type": "field", 458 + "named": true 459 + } 460 + ] 461 + } 462 + }, 463 + { 464 + "type": "record_definition", 465 + "named": true, 466 + "fields": { 467 + "body": { 468 + "multiple": false, 469 + "required": true, 470 + "types": [ 471 + { 472 + "type": "record_body", 473 + "named": true 474 + } 475 + ] 476 + }, 477 + "name": { 478 + "multiple": false, 479 + "required": true, 480 + "types": [ 481 + { 482 + "type": "identifier", 483 + "named": true 484 + } 485 + ] 486 + } 487 + } 488 + }, 489 + { 490 + "type": "reference_type", 491 + "named": true, 492 + "fields": {}, 493 + "children": { 494 + "multiple": false, 495 + "required": true, 496 + "types": [ 497 + { 498 + "type": "type_path", 499 + "named": true 500 + } 501 + ] 502 + } 503 + }, 504 + { 505 + "type": "return_type", 506 + "named": true, 507 + "fields": {}, 508 + "children": { 509 + "multiple": true, 510 + "required": true, 511 + "types": [ 512 + { 513 + "type": "error_definition", 514 + "named": true 515 + }, 516 + { 517 + "type": "type", 518 + "named": true 519 + } 520 + ] 521 + } 522 + }, 523 + { 524 + "type": "source_file", 525 + "named": true, 526 + "fields": {}, 527 + "children": { 528 + "multiple": true, 529 + "required": false, 530 + "types": [ 531 + { 532 + "type": "item", 533 + "named": true 534 + } 535 + ] 536 + } 537 + }, 538 + { 539 + "type": "subscription_definition", 540 + "named": true, 541 + "fields": { 542 + "messages": { 543 + "multiple": false, 544 + "required": true, 545 + "types": [ 546 + { 547 + "type": "type", 548 + "named": true 549 + } 550 + ] 551 + }, 552 + "name": { 553 + "multiple": false, 554 + "required": true, 555 + "types": [ 556 + { 557 + "type": "identifier", 558 + "named": true 559 + } 560 + ] 561 + }, 562 + "params": { 563 + "multiple": false, 564 + "required": true, 565 + "types": [ 566 + { 567 + "type": "parameter_list", 568 + "named": true 569 + } 570 + ] 571 + } 572 + } 573 + }, 574 + { 575 + "type": "token_definition", 576 + "named": true, 577 + "fields": { 578 + "name": { 579 + "multiple": false, 580 + "required": true, 581 + "types": [ 582 + { 583 + "type": "identifier", 584 + "named": true 585 + } 586 + ] 587 + } 588 + } 589 + }, 590 + { 591 + "type": "type", 592 + "named": true, 593 + "fields": {}, 594 + "children": { 595 + "multiple": false, 596 + "required": true, 597 + "types": [ 598 + { 599 + "type": "non_union_type", 600 + "named": true 601 + }, 602 + { 603 + "type": "union_type", 604 + "named": true 605 + } 606 + ] 607 + } 608 + }, 609 + { 610 + "type": "type_path", 611 + "named": true, 612 + "fields": {}, 613 + "children": { 614 + "multiple": true, 615 + "required": true, 616 + "types": [ 617 + { 618 + "type": "identifier", 619 + "named": true 620 + } 621 + ] 622 + } 623 + }, 624 + { 625 + "type": "union_type", 626 + "named": true, 627 + "fields": {}, 628 + "children": { 629 + "multiple": true, 630 + "required": true, 631 + "types": [ 632 + { 633 + "type": "non_union_type", 634 + "named": true 635 + } 636 + ] 637 + } 638 + }, 639 + { 640 + "type": "use_statement", 641 + "named": true, 642 + "fields": { 643 + "path": { 644 + "multiple": false, 645 + "required": true, 646 + "types": [ 647 + { 648 + "type": "type_path", 649 + "named": true 650 + } 651 + ] 652 + } 653 + } 654 + }, 655 + { 656 + "type": "(", 657 + "named": false 658 + }, 659 + { 660 + "type": ")", 661 + "named": false 662 + }, 663 + { 664 + "type": ",", 665 + "named": false 666 + }, 667 + { 668 + "type": "->", 669 + "named": false 670 + }, 671 + { 672 + "type": ".", 673 + "named": false 674 + }, 675 + { 676 + "type": ":", 677 + "named": false 678 + }, 679 + { 680 + "type": ";", 681 + "named": false 682 + }, 683 + { 684 + "type": "=", 685 + "named": false 686 + }, 687 + { 688 + "type": "?", 689 + "named": false 690 + }, 691 + { 692 + "type": "[", 693 + "named": false 694 + }, 695 + { 696 + "type": "]", 697 + "named": false 698 + }, 699 + { 700 + "type": "`", 701 + "named": false 702 + }, 703 + { 704 + "type": "alias", 705 + "named": false 706 + }, 707 + { 708 + "type": "blob", 709 + "named": false 710 + }, 711 + { 712 + "type": "boolean", 713 + "named": false 714 + }, 715 + { 716 + "type": "bytes", 717 + "named": false 718 + }, 719 + { 720 + "type": "comment", 721 + "named": true 722 + }, 723 + { 724 + "type": "constrained", 725 + "named": false 726 + }, 727 + { 728 + "type": "doc_comment", 729 + "named": true 730 + }, 731 + { 732 + "type": "false", 733 + "named": false 734 + }, 735 + { 736 + "type": "integer", 737 + "named": false 738 + }, 739 + { 740 + "type": "namespace", 741 + "named": false 742 + }, 743 + { 744 + "type": "namespace_identifier", 745 + "named": true 746 + }, 747 + { 748 + "type": "null", 749 + "named": false 750 + }, 751 + { 752 + "type": "number", 753 + "named": true 754 + }, 755 + { 756 + "type": "number", 757 + "named": false 758 + }, 759 + { 760 + "type": "procedure", 761 + "named": false 762 + }, 763 + { 764 + "type": "query", 765 + "named": false 766 + }, 767 + { 768 + "type": "record", 769 + "named": false 770 + }, 771 + { 772 + "type": "string", 773 + "named": true 774 + }, 775 + { 776 + "type": "string", 777 + "named": false 778 + }, 779 + { 780 + "type": "subscription", 781 + "named": false 782 + }, 783 + { 784 + "type": "throws", 785 + "named": false 786 + }, 787 + { 788 + "type": "token", 789 + "named": false 790 + }, 791 + { 792 + "type": "true", 793 + "named": false 794 + }, 795 + { 796 + "type": "unknown", 797 + "named": false 798 + }, 799 + { 800 + "type": "use", 801 + "named": false 802 + }, 803 + { 804 + "type": "{", 805 + "named": false 806 + }, 807 + { 808 + "type": "|", 809 + "named": false 810 + }, 811 + { 812 + "type": "}", 813 + "named": false 814 + } 815 + ]
+10
tree-sitter-mlf/test.mlf
··· 1 + /// A simple post record 2 + record post { 3 + /// Post text 4 + text: string constrained { 5 + maxLength: 300, 6 + minLength: 1, 7 + }, 8 + /// Creation timestamp 9 + createdAt: Datetime, 10 + };
+9
website/.gitignore
··· 1 + # Generated WASM files 2 + static/js/pkg/ 3 + 4 + # Zola output 5 + public/ 6 + 7 + # Local dev files 8 + .DS_Store 9 + *.log
+152
website/README.md
··· 1 + # MLF Website 2 + 3 + Static website for MLF (Matt's Lexicon Format) with interactive playground powered by WebAssembly. 4 + 5 + ## Features 6 + 7 + - 📄 Landing page with project overview 8 + - 📚 Documentation (to be added) 9 + - 🎮 Interactive playground for editing and testing MLF 10 + - 🚀 Client-side WASM for instant validation and generation 11 + - 🎨 No external dependencies - pure HTML/CSS/JS 12 + 13 + ## Building the WASM Module 14 + 15 + ### Prerequisites 16 + 17 + ```bash 18 + # Install wasm-pack 19 + cargo install wasm-pack 20 + ``` 21 + 22 + ### Build 23 + 24 + ```bash 25 + # From the project root 26 + wasm-pack build mlf-wasm --target web --out-dir ../website/js/pkg 27 + 28 + # Or with optimization 29 + wasm-pack build mlf-wasm --target web --out-dir ../website/js/pkg --release 30 + ``` 31 + 32 + This will generate: 33 + - `website/js/pkg/mlf_wasm.js` - JavaScript bindings 34 + - `website/js/pkg/mlf_wasm_bg.wasm` - WASM binary 35 + - `website/js/pkg/mlf_wasm.d.ts` - TypeScript definitions 36 + 37 + ### Update the JavaScript 38 + 39 + After building, update `js/app.js` to import the WASM module: 40 + 41 + ```javascript 42 + // Replace the init() function with: 43 + async function init() { 44 + try { 45 + const wasmModule = await import('./pkg/mlf_wasm.js'); 46 + await wasmModule.default(); 47 + wasm = wasmModule; 48 + 49 + setupEventListeners(); 50 + // Initial generation 51 + handleCheck(); 52 + } catch (error) { 53 + console.error('Failed to load WASM module:', error); 54 + showError('Failed to load MLF WASM module.'); 55 + } 56 + } 57 + ``` 58 + 59 + ## Development 60 + 61 + ### Local Server 62 + 63 + You need a local server to serve the WASM files (due to CORS restrictions): 64 + 65 + ```bash 66 + # Python 3 67 + cd website 68 + python3 -m http.server 8000 69 + 70 + # Or use any static file server 71 + npx serve website 72 + ``` 73 + 74 + Then open http://localhost:8000 75 + 76 + ## Deployment 77 + 78 + The website is completely static and can be deployed to: 79 + 80 + - **GitHub Pages**: Push to `gh-pages` branch 81 + - **Netlify**: Drag and drop the `website/` folder 82 + - **Vercel**: Connect to repository 83 + - **Any static host**: Upload the files 84 + 85 + ### GitHub Pages Example 86 + 87 + ```bash 88 + # Build WASM 89 + wasm-pack build mlf-wasm --target web --out-dir ../website/js/pkg --release 90 + 91 + # Deploy (from website directory) 92 + git subtree push --prefix website origin gh-pages 93 + ``` 94 + 95 + ## Project Structure 96 + 97 + ``` 98 + website/ 99 + ├── index.html # Main landing page 100 + ├── css/ 101 + │ └── style.css # Styles 102 + ├── js/ 103 + │ ├── app.js # Main application logic 104 + │ └── pkg/ # WASM module (generated) 105 + └── docs/ # Documentation pages (to be added) 106 + ``` 107 + 108 + ## WASM API 109 + 110 + The WASM module exposes these functions: 111 + 112 + ### `parse(source: string) -> ParseResult` 113 + Parses MLF source and returns whether it's valid. 114 + 115 + ### `check(source: string) -> CheckResult` 116 + Checks MLF source for syntax and validation errors. 117 + 118 + ### `generate_lexicon(source: string, namespace: string) -> GenerateResult` 119 + Generates a JSON lexicon from MLF source. 120 + 121 + ### `validate_record(lexicon_source: string, record_json: string) -> ValidateResult` 122 + Validates a JSON record against an MLF lexicon schema. 123 + 124 + ## Browser Compatibility 125 + 126 + The WASM module requires: 127 + - WebAssembly support (all modern browsers) 128 + - ES6 modules support 129 + - Async/await support 130 + 131 + Works in: 132 + - Chrome/Edge 61+ 133 + - Firefox 60+ 134 + - Safari 11+ 135 + 136 + ## Size Optimization 137 + 138 + To reduce WASM bundle size: 139 + 140 + ```bash 141 + # Build with optimizations 142 + wasm-pack build mlf-wasm --target web --release 143 + 144 + # Further optimize with wasm-opt (from binaryen) 145 + wasm-opt -Oz website/js/pkg/mlf_wasm_bg.wasm -o website/js/pkg/mlf_wasm_bg.wasm 146 + ``` 147 + 148 + Expected sizes: 149 + - Unoptimized: ~200KB 150 + - Release: ~100KB 151 + - With wasm-opt: ~80KB 152 + - Gzipped: ~30KB
+16
website/config.toml
··· 1 + base_url = "https://mlf.lol" 2 + title = "MLF - Matt's Lexicon Format" 3 + description = "A human-friendly DSL for ATProto Lexicons" 4 + default_language = "en" 5 + 6 + compile_sass = true 7 + build_search_index = false 8 + generate_feeds = false 9 + 10 + [markdown] 11 + highlight_code = true 12 + highlight_theme = "dracula" 13 + extra_syntaxes_and_themes = ["syntaxes"] 14 + 15 + [extra] 16 + github_url = "https://tangled.org/@stavola.xyz/mlf"
+59
website/content/_index.md
··· 1 + +++ 2 + title = "MLF - Matt's Lexicon Format" 3 + description = "A human-friendly DSL for ATProto Lexicons" 4 + template = "index.html" 5 + 6 + [extra] 7 + mlf_example = '''```mlf 8 + /// A forum thread 9 + record thread { 10 + /// Thread title 11 + title: string constrained { 12 + maxLength: 200, 13 + minLength: 1, 14 + }, 15 + /// Thread body content 16 + body: string constrained { 17 + maxLength: 10000, 18 + }, 19 + /// Thread creation timestamp 20 + createdAt: Datetime, 21 + }; 22 + ```''' 23 + 24 + json_example = '''```json 25 + { 26 + "lexicon": 1, 27 + "id": "com.example.thread", 28 + "defs": { 29 + "main": { 30 + "type": "record", 31 + "description": "A forum thread", 32 + "key": "tid", 33 + "record": { 34 + "type": "object", 35 + "required": ["title", "body", "createdAt"], 36 + "properties": { 37 + "title": { 38 + "type": "string", 39 + "description": "Thread title", 40 + "maxLength": 200, 41 + "minLength": 1 42 + }, 43 + "body": { 44 + "type": "string", 45 + "description": "Thread body content", 46 + "maxLength": 10000 47 + }, 48 + "createdAt": { 49 + "type": "string", 50 + "format": "datetime", 51 + "description": "Thread creation timestamp" 52 + } 53 + } 54 + } 55 + } 56 + } 57 + } 58 + ```''' 59 + +++
+12
website/content/docs/_index.md
··· 1 + +++ 2 + title = "Documentation" 3 + description = "Complete guide to MLF" 4 + sort_by = "weight" 5 + template = "section.html" 6 + +++ 7 + 8 + MLF (Matt's Lexicon Format) is a human-friendly DSL for writing ATProto Lexicons. This documentation will help you learn the language, use the CLI tools, and integrate MLF into your projects. 9 + 10 + ## Quick Links 11 + 12 + Start with the **Getting Started** guide to install MLF and write your first lexicon, then explore the other sections to learn more about the language and tools.
+145
website/content/docs/cli.md
··· 1 + +++ 2 + title = "CLI Reference" 3 + description = "Command-line tool documentation" 4 + weight = 3 5 + +++ 6 + 7 + ## Installation 8 + 9 + Clone and build from source: 10 + 11 + ```bash 12 + git clone https://tangled.org/@stavola.xyz/mlf 13 + cd mlf 14 + cargo build --release 15 + ``` 16 + 17 + The binary will be at `target/release/mlf`. 18 + 19 + Optionally, install to your PATH: 20 + 21 + ```bash 22 + # Option 1: Use cargo install 23 + cargo install --path mlf-cli 24 + 25 + # Option 2: Manually copy the binary 26 + cp target/release/mlf /usr/local/bin/ 27 + ``` 28 + 29 + ## Commands 30 + 31 + ### `mlf check` 32 + 33 + Validate MLF lexicon files for syntax and type errors. 34 + 35 + ```bash 36 + mlf check [INPUT]... 37 + ``` 38 + 39 + **Arguments:** 40 + - `[INPUT]...` - MLF lexicon file(s) to validate (glob patterns supported) 41 + 42 + **Examples:** 43 + 44 + ```bash 45 + # Check a single file 46 + mlf check thread.mlf 47 + 48 + # Check multiple files 49 + mlf check thread.mlf profile.mlf reply.mlf 50 + 51 + # Check with glob patterns 52 + mlf check "lexicons/**/*.mlf" 53 + ``` 54 + 55 + **Output:** 56 + - ✓ Success message if valid 57 + - Detailed error messages with source context if invalid 58 + 59 + --- 60 + 61 + ### `mlf validate` 62 + 63 + Validate a JSON record against an MLF lexicon. 64 + 65 + ```bash 66 + mlf validate <LEXICON> <RECORD> 67 + ``` 68 + 69 + **Arguments:** 70 + - `<LEXICON>` - MLF lexicon file 71 + - `<RECORD>` - JSON record file to validate against the lexicon 72 + 73 + **Example:** 74 + 75 + ```bash 76 + mlf validate thread.mlf record.json 77 + ``` 78 + 79 + **Output:** 80 + - ✓ Success if record is valid 81 + - Detailed validation errors if invalid 82 + 83 + --- 84 + 85 + ### `mlf generate lexicon` 86 + 87 + Generate ATProto JSON lexicons from MLF files. 88 + 89 + ```bash 90 + mlf generate lexicon --output <OUTPUT> [OPTIONS] 91 + ``` 92 + 93 + **Options:** 94 + - `-i, --input <INPUT>` - Input MLF files (glob patterns supported, can be specified multiple times) 95 + - `-o, --output <OUTPUT>` - Output directory (required) 96 + - `--flat` - Use flat file structure (e.g., `com.example.thread.json`) 97 + 98 + **Examples:** 99 + 100 + ```bash 101 + # Generate with folder structure 102 + mlf generate lexicon -i thread.mlf -o lexicons/ 103 + # Creates: lexicons/com/example/thread.json 104 + 105 + # Generate with flat structure 106 + mlf generate lexicon -i thread.mlf -o lexicons/ --flat 107 + # Creates: lexicons/com.example.thread.json 108 + 109 + # Generate from multiple files 110 + mlf generate lexicon -i thread.mlf -i reply.mlf -o lexicons/ 111 + 112 + # Generate from glob pattern 113 + mlf generate lexicon -i "src/**/*.mlf" -o dist/lexicons/ 114 + ``` 115 + 116 + --- 117 + 118 + ## Error Messages 119 + 120 + MLF provides rich error messages with: 121 + 122 + - Source code context 123 + - Labeled spans showing exact error locations 124 + - Helpful suggestions for fixing errors 125 + - Error codes for categorization 126 + 127 + **Example error:** 128 + 129 + ``` 130 + × Undefined reference to 'ProfileView' 131 + ╭─[profile.mlf:5:12] 132 + 5 │ author: ProfileView, 133 + · ^^^^^^^^^^^ 'ProfileView' is not defined 134 + ╰──── 135 + help: Make sure this type is defined in the same file or imported via 'use'. 136 + ``` 137 + 138 + ## Environment Variables 139 + 140 + None currently used. 141 + 142 + ## Exit Codes 143 + 144 + - `0` - Success 145 + - `1` - Error (syntax, validation, or runtime error)
+88
website/content/docs/getting-started.md
··· 1 + +++ 2 + title = "Getting Started" 3 + description = "Install MLF and write your first lexicon" 4 + weight = 1 5 + +++ 6 + 7 + ## Installation 8 + 9 + Clone and build from source: 10 + 11 + ```bash 12 + git clone https://tangled.org/@stavola.xyz/mlf 13 + cd mlf 14 + cargo build --release 15 + ``` 16 + 17 + The binary will be at `target/release/mlf`. 18 + 19 + Optionally, install to your PATH: 20 + 21 + ```bash 22 + # Option 1: Use cargo install 23 + cargo install --path mlf-cli 24 + 25 + # Option 2: Manually copy the binary 26 + cp target/release/mlf /usr/local/bin/ 27 + ``` 28 + 29 + ## Your First Lexicon 30 + 31 + Create a file `thread.mlf`: 32 + 33 + ```mlf 34 + /// A forum thread 35 + record thread { 36 + /// Thread title 37 + title: string constrained { 38 + maxLength: 200, 39 + minLength: 1, 40 + }, 41 + /// Thread body 42 + body: string constrained { 43 + maxLength: 10000, 44 + }, 45 + /// Thread creation timestamp 46 + createdAt: Datetime, 47 + }; 48 + ``` 49 + 50 + ## Generate JSON Lexicon 51 + 52 + ```bash 53 + mlf generate lexicon -i thread.mlf -o lexicons/ 54 + ``` 55 + 56 + This creates `lexicons/com/example/thread.json` with the ATProto JSON lexicon. 57 + 58 + ## Validate MLF Files 59 + 60 + ```bash 61 + mlf check thread.mlf 62 + ``` 63 + 64 + This checks your MLF file for syntax and validation errors. 65 + 66 + ## Validate Records 67 + 68 + Given a JSON record file `record.json`: 69 + 70 + ```json 71 + { 72 + "title": "Welcome to the forums!", 73 + "body": "This is my first thread. Looking forward to discussions!", 74 + "createdAt": "2024-01-15T10:30:00Z" 75 + } 76 + ``` 77 + 78 + Validate it against your lexicon: 79 + 80 + ```bash 81 + mlf validate thread.mlf record.json 82 + ``` 83 + 84 + ## Next Steps 85 + 86 + - Read the [Syntax Guide](./syntax.md) to learn the MLF language 87 + - Check out the [CLI Reference](./cli.md) for all commands 88 + - Try the [WASM API](./wasm.md) to use MLF in the browser
+550
website/content/docs/syntax.md
··· 1 + +++ 2 + title = "Language Syntax" 3 + description = "Complete reference for MLF syntax and features" 4 + weight = 2 5 + +++ 6 + 7 + ## File Structure 8 + 9 + ### File Extension 10 + - `.mlf` - MLF source files 11 + 12 + ### Shebang (Optional) 13 + ```mlf 14 + #!/usr/bin/env mlf 15 + ``` 16 + 17 + ### File Naming Convention 18 + Files should follow the lexicon NSID: 19 + - `com.example.forum.thread.mlf` → Lexicon NSID: `com.example.forum.thread` 20 + - `com.example.user.profile.mlf` → Lexicon NSID: `com.example.user.profile` 21 + 22 + ## Basic Structure 23 + 24 + Every MLF file can contain: 25 + 26 + - Namespace declarations 27 + - Use statements (imports) 28 + - Type definitions (record, alias, token, query, procedure, subscription) 29 + 30 + ## Primitive Types 31 + 32 + - `null` - Null value 33 + - `boolean` - True/false 34 + - `integer` - 64-bit integer 35 + - `number` - Double-precision float 36 + - `string` - UTF-8 string 37 + - `bytes` - Byte array 38 + - `blob` - Binary large object with metadata 39 + - `unknown` - Any value (for forward compatibility) 40 + 41 + ## Special String Formats 42 + 43 + These are defined in the prelude and available everywhere: 44 + 45 + - `Did` - Decentralized Identifier (did:*) 46 + - `AtUri` - AT-URI (at://...) 47 + - `AtIdentifier` - Either a DID or Handle 48 + - `Handle` - Handle identifier (domain name) 49 + - `Datetime` - ISO 8601 datetime 50 + - `Uri` - Generic URI 51 + - `Cid` - Content Identifier 52 + - `Nsid` - Namespaced Identifier 53 + - `Tid` - Timestamp Identifier 54 + - `RecordKey` - Record key 55 + - `Language` - BCP 47 language code 56 + 57 + ## Records 58 + 59 + Records define structured data types stored in repositories: 60 + 61 + ```mlf 62 + /// A forum thread 63 + record thread { 64 + /// Thread title 65 + title: string constrained { 66 + maxLength: 200, 67 + minLength: 1, 68 + }, 69 + /// Thread body 70 + body?: string, // Optional field 71 + /// Thread creation timestamp 72 + createdAt: Datetime, 73 + }; 74 + ``` 75 + 76 + ## Aliases 77 + 78 + Type aliases define reusable object shapes: 79 + 80 + ```mlf 81 + alias replyRef = { 82 + root: AtUri, 83 + parent: AtUri, 84 + }; 85 + 86 + record thread { 87 + reply?: replyRef, 88 + }; 89 + ``` 90 + 91 + If used in multiple places, they will be hoisted to a def. If only used once, they will be inlined. 92 + 93 + ## Tokens 94 + 95 + Tokens are named constants used in enums and unions: 96 + 97 + ```mlf 98 + /// Open state 99 + token open; 100 + 101 + /// Closed state 102 + token closed; 103 + 104 + record issue { 105 + state: string constrained { 106 + knownValues: [open, closed], 107 + default: "open", 108 + }, 109 + }; 110 + ``` 111 + 112 + Tokens must have doc comments describing their purpose. 113 + 114 + ## Constrained Types 115 + 116 + Add validation constraints to types: 117 + 118 + ```mlf 119 + title: string constrained { 120 + maxLength: 200, 121 + minLength: 1, 122 + }; 123 + 124 + age: integer constrained { 125 + minimum: 0, 126 + maximum: 150, 127 + }; 128 + 129 + status: string constrained { 130 + enum: ["draft", "published", "archived"], 131 + }; 132 + ``` 133 + 134 + ### String Constraints 135 + 136 + - `maxLength` / `minLength` - Length in bytes 137 + - `maxGraphemes` / `minGraphemes` - Length in grapheme clusters 138 + - `format` - Format validation (datetime, uri, did, handle, etc.) 139 + - `enum` - Allowed values (closed set) 140 + - `knownValues` - Known values (extensible set, can reference tokens) 141 + - `default` - Default value 142 + 143 + ### Integer Constraints 144 + 145 + - `minimum` / `maximum` - Min/max values 146 + - `enum` - Allowed values 147 + - `default` - Default value 148 + 149 + ### Array Constraints 150 + 151 + ```mlf 152 + tags: string[] constrained { 153 + minLength: 1, 154 + maxLength: 10, 155 + } 156 + ``` 157 + 158 + ### Blob Constraints 159 + 160 + ```mlf 161 + avatar: blob constrained { 162 + accept: ["image/png", "image/jpeg"], 163 + maxSize: 1000000, // bytes 164 + } 165 + ``` 166 + 167 + ### Boolean Constraints 168 + 169 + ```mlf 170 + field: boolean constrained { 171 + default: false, 172 + } 173 + ``` 174 + 175 + ### Constraint Refinement 176 + 177 + Constraints can only make types **more restrictive**, never less restrictive: 178 + 179 + ```mlf 180 + alias shortString = string constrained { 181 + maxLength: 100, 182 + }; 183 + 184 + record post { 185 + // Valid: 50 is more restrictive than 100 186 + title: shortString constrained { 187 + maxLength: 50, 188 + }, 189 + }; 190 + ``` 191 + 192 + **Refinement rules:** 193 + - Numeric bounds: `minimum` can only increase, `maximum` can only decrease 194 + - Length bounds: `minLength`/`minGraphemes` can only increase, `maxLength`/`maxGraphemes` can only decrease 195 + - Enums: Can only restrict to a subset 196 + - Format: Cannot change once specified 197 + 198 + ## Arrays 199 + 200 + ```mlf 201 + tags: string[] 202 + 203 + items: string[] constrained { 204 + minLength: 1, 205 + maxLength: 10, 206 + } 207 + ``` 208 + 209 + ## Unions 210 + 211 + Use the pipe operator `|`: 212 + 213 + ```mlf 214 + // Closed union (only these types) 215 + content: text | image | video 216 + 217 + // Union of tokens 218 + state: open | closed | pending 219 + ``` 220 + 221 + Open unions (allowing unknown types) use `_`: 222 + 223 + ```mlf 224 + // Open union (can include unknown types) 225 + content: text | image | _ 226 + ``` 227 + 228 + ## Objects 229 + 230 + Inline object types: 231 + 232 + ```mlf 233 + metadata: { 234 + version: integer, 235 + timestamp: Datetime, 236 + } 237 + ``` 238 + 239 + ## Queries 240 + 241 + Queries are read-only HTTP endpoints (GET): 242 + 243 + ```mlf 244 + /// Get a user profile 245 + query getProfile( 246 + /// The actor's DID or handle 247 + actor: AtIdentifier, 248 + ): profile; 249 + ``` 250 + 251 + With errors: 252 + 253 + ```mlf 254 + query getThread( 255 + uri: AtUri, 256 + ): thread | error { 257 + /// Thread not found 258 + NotFound, 259 + /// Invalid request 260 + BadRequest, 261 + }; 262 + ``` 263 + 264 + ## Procedures 265 + 266 + Procedures are write operations (POST): 267 + 268 + ```mlf 269 + /// Create a new thread 270 + procedure createThread( 271 + title: string, 272 + body: string, 273 + ): { 274 + uri: AtUri, 275 + cid: Cid, 276 + } | error { 277 + /// Title too long 278 + TitleTooLong, 279 + }; 280 + ``` 281 + 282 + ## Subscriptions 283 + 284 + Subscriptions are WebSocket-based event streams: 285 + 286 + ```mlf 287 + /// Subscribe to repository events 288 + subscription subscribeRepos( 289 + /// Optional cursor for resuming 290 + cursor?: integer, 291 + ): commit | identity | handle; 292 + ``` 293 + 294 + Message types must be defined as aliases or records: 295 + 296 + ```mlf 297 + /// Commit message 298 + alias commit = { 299 + seq: integer, 300 + repo: Did, 301 + commit: Cid, 302 + time: Datetime, 303 + }; 304 + 305 + /// Identity message 306 + alias identity = { 307 + did: Did, 308 + handle: Handle, 309 + }; 310 + ``` 311 + 312 + ## Comments 313 + 314 + ### Documentation Comments 315 + 316 + Use `///` for documentation (appears in generated docs/code): 317 + 318 + ```mlf 319 + /// A forum thread 320 + record thread { 321 + /// Thread title 322 + title: string, 323 + }; 324 + ``` 325 + 326 + ### Regular Comments 327 + 328 + Use `//` for comments that won't appear in output: 329 + 330 + ```mlf 331 + // This is a regular comment 332 + record example { 333 + field: string, // inline comment 334 + }; 335 + ``` 336 + 337 + ## Annotations 338 + 339 + Annotations use `@` and provide metadata for external tooling: 340 + 341 + ### Simple Annotation 342 + ```mlf 343 + @deprecated 344 + record oldRecord { 345 + field: string, 346 + } 347 + ``` 348 + 349 + ### Positional Arguments 350 + ```mlf 351 + @since(1, 2, 0) 352 + @doc("https://example.com/docs") 353 + record example { 354 + field: string, 355 + } 356 + ``` 357 + 358 + ### Named Arguments 359 + ```mlf 360 + @validate(min: 0, max: 100, strict: true) 361 + @table(name: "threads", indexes: "did,createdAt") 362 + record thread { 363 + @indexed 364 + did: Did, 365 + 366 + @sensitive(pii: true) 367 + title: string, 368 + } 369 + ``` 370 + 371 + Annotations can be placed on records, aliases, tokens, queries, procedures, subscriptions, and fields. 372 + 373 + ## Imports 374 + 375 + Import definitions from other lexicons: 376 + 377 + ```mlf 378 + // Single import 379 + use com.example.user.profile; 380 + 381 + // Multiple imports 382 + use com.example.forum.{thread, reply}; 383 + 384 + // Alias import 385 + use com.example.user as User; 386 + 387 + // Wildcard import 388 + use com.example.forum.*; 389 + 390 + // Import with alias 391 + use com.example.forum.{thread as ForumThread}; 392 + ``` 393 + 394 + After importing, use the short name: 395 + 396 + ```mlf 397 + use com.example.user.profile; 398 + 399 + 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 + } 433 + } 434 + ``` 435 + 436 + ## References 437 + 438 + Reference local or external definitions: 439 + 440 + ```mlf 441 + // Local reference (same file) 442 + record thread { 443 + author: author, // References 'alias author' in same file 444 + } 445 + 446 + // Cross-file reference 447 + record thread { 448 + profile: com.example.user.profile, // References com/example/user/profile.mlf 449 + } 450 + ``` 451 + 452 + **Note:** All references use dotted notation. The `#` character is NOT used for references. 453 + 454 + ## Optional Fields 455 + 456 + Use `?` to mark fields as optional: 457 + 458 + ```mlf 459 + record thread { 460 + title: string, // Required 461 + body?: string, // Optional 462 + tags?: string[], // Optional array 463 + } 464 + ``` 465 + 466 + ## Raw Identifiers 467 + 468 + Use backticks to escape reserved keywords when you need to use them as identifiers: 469 + 470 + ```mlf 471 + alias `record` = { 472 + `record`: com.atproto.repo.strongRef, 473 + `error`: string, 474 + }; 475 + ``` 476 + 477 + This is useful when working with existing schemas that use MLF keywords as field or type names. 478 + 479 + ## Format Strings 480 + 481 + Available format strings for constrained strings: 482 + 483 + - `datetime` - ISO 8601 datetime 484 + - `uri` - URI (RFC 3986) 485 + - `at-uri` - AT-URI (ATProto) 486 + - `did` - Decentralized Identifier 487 + - `handle` - ATProto handle 488 + - `nsid` - Namespaced Identifier 489 + - `cid` - Content Identifier 490 + - `at-identifier` - DID or handle 491 + - `language` - BCP 47 language tag 492 + - `tid` - Timestamp ID 493 + - `record-key` - Record key 494 + 495 + ## Complete Example 496 + 497 + ```mlf 498 + #!/usr/bin/env mlf 499 + 500 + use com.example.user.profile; 501 + 502 + /// Open state 503 + token open; 504 + 505 + /// Closed state 506 + token closed; 507 + 508 + /// A forum thread 509 + record thread { 510 + /// Thread title 511 + title: string constrained { 512 + minGraphemes: 1, 513 + maxGraphemes: 200, 514 + }, 515 + /// Thread body (markdown) 516 + body?: string constrained { 517 + maxGraphemes: 10000, 518 + }, 519 + /// Thread state 520 + state: string constrained { 521 + knownValues: [open, closed], 522 + default: "open", 523 + }, 524 + /// Author profile 525 + author: profile, 526 + /// Creation timestamp 527 + createdAt: Datetime, 528 + }; 529 + 530 + /// Get a thread by URI 531 + query getThread( 532 + /// Thread AT-URI 533 + uri: AtUri, 534 + ): thread | error { 535 + /// Thread not found 536 + NotFound, 537 + }; 538 + 539 + /// Create a new thread 540 + procedure createThread( 541 + title: string, 542 + body?: string, 543 + ): { 544 + uri: AtUri, 545 + cid: Cid, 546 + } | error { 547 + /// Title too long 548 + TitleTooLong, 549 + }; 550 + ```
+206
website/content/docs/wasm.md
··· 1 + +++ 2 + title = "WASM API" 3 + description = "Using MLF in the browser with WebAssembly" 4 + weight = 4 5 + +++ 6 + 7 + ## Installation 8 + 9 + Build from source: 10 + 11 + ```bash 12 + # Clone the repository 13 + git clone https://tangled.org/@stavola.xyz/mlf 14 + cd mlf 15 + 16 + # Install wasm-pack if you don't have it 17 + cargo install wasm-pack 18 + 19 + # Build the WASM module 20 + wasm-pack build mlf-wasm --target web 21 + ``` 22 + 23 + This generates: 24 + - `pkg/mlf_wasm.js` - JavaScript bindings 25 + - `pkg/mlf_wasm_bg.wasm` - WebAssembly binary 26 + - `pkg/mlf_wasm.d.ts` - TypeScript definitions 27 + 28 + ## Usage 29 + 30 + ### Loading the Module 31 + 32 + ```javascript 33 + import init, * as mlf from './pkg/mlf_wasm.js'; 34 + 35 + // Initialize the WASM module 36 + await init(); 37 + 38 + // Now you can use MLF functions 39 + ``` 40 + 41 + ### API Functions 42 + 43 + #### `parse(source: string) -> ParseResult` 44 + 45 + Parse MLF source and check for syntax errors. 46 + 47 + ```javascript 48 + const result = mlf.parse(` 49 + record post { 50 + text: string, 51 + }; 52 + `); 53 + 54 + if (result.success) { 55 + console.log('Valid MLF!'); 56 + } else { 57 + console.error('Parse error:', result.error); 58 + } 59 + ``` 60 + 61 + **Returns:** 62 + ```typescript 63 + { 64 + success: boolean, 65 + error?: string 66 + } 67 + ``` 68 + 69 + --- 70 + 71 + #### `check(source: string) -> CheckResult` 72 + 73 + Perform full validation (parse + type checking). 74 + 75 + ```javascript 76 + const result = mlf.check(mlfSource); 77 + 78 + if (result.success) { 79 + console.log('Lexicon is valid!'); 80 + } else { 81 + console.error('Errors:', result.errors); 82 + } 83 + ``` 84 + 85 + **Returns:** 86 + ```typescript 87 + { 88 + success: boolean, 89 + errors: string[] 90 + } 91 + ``` 92 + 93 + --- 94 + 95 + #### `generate_lexicon(source: string, namespace: string) -> GenerateResult` 96 + 97 + Generate a JSON lexicon from MLF source. 98 + 99 + ```javascript 100 + const result = mlf.generate_lexicon(mlfSource, 'com.example.thread'); 101 + 102 + if (result.success) { 103 + const lexicon = JSON.parse(result.lexicon); 104 + console.log(lexicon); 105 + } else { 106 + console.error('Generation error:', result.error); 107 + } 108 + ``` 109 + 110 + **Returns:** 111 + ```typescript 112 + { 113 + success: boolean, 114 + lexicon?: string, // JSON string 115 + error?: string 116 + } 117 + ``` 118 + 119 + --- 120 + 121 + #### `validate_record(lexicon: string, record: string) -> ValidateResult` 122 + 123 + Validate a JSON record against an MLF lexicon. 124 + 125 + ```javascript 126 + const record = JSON.stringify({ 127 + text: "Hello, world!", 128 + createdAt: "2024-01-15T10:30:00Z" 129 + }); 130 + 131 + const result = mlf.validate_record(mlfSource, record); 132 + 133 + if (result.success) { 134 + console.log('Record is valid!'); 135 + } else { 136 + console.error('Validation errors:', result.errors); 137 + } 138 + ``` 139 + 140 + **Returns:** 141 + ```typescript 142 + { 143 + success: boolean, 144 + errors: string[] 145 + } 146 + ``` 147 + 148 + --- 149 + 150 + ## Example: Live Editor 151 + 152 + ```html 153 + <!DOCTYPE html> 154 + <html> 155 + <head> 156 + <title>MLF Editor</title> 157 + </head> 158 + <body> 159 + <textarea id="editor"></textarea> 160 + <pre id="output"></pre> 161 + 162 + <script type="module"> 163 + import init, * as mlf from './pkg/mlf_wasm.js'; 164 + 165 + await init(); 166 + 167 + const editor = document.getElementById('editor'); 168 + const output = document.getElementById('output'); 169 + 170 + editor.addEventListener('input', () => { 171 + const source = editor.value; 172 + const result = mlf.generate_lexicon(source, 'app.example'); 173 + 174 + if (result.success) { 175 + output.textContent = JSON.stringify( 176 + JSON.parse(result.lexicon), 177 + null, 178 + 2 179 + ); 180 + } else { 181 + output.textContent = `Error: ${result.error}`; 182 + } 183 + }); 184 + </script> 185 + </body> 186 + </html> 187 + ``` 188 + 189 + ## Browser Compatibility 190 + 191 + Requires: 192 + - WebAssembly support 193 + - ES6 modules 194 + - Async/await 195 + 196 + Supported browsers: 197 + - Chrome/Edge 61+ 198 + - Firefox 60+ 199 + - Safari 11+ 200 + 201 + ## Bundle Size 202 + 203 + Typical sizes (with wasm-opt): 204 + - WASM: ~80KB 205 + - JS: ~5KB 206 + - Gzipped: ~30KB total
+9
website/content/playground.md
··· 1 + +++ 2 + title = "Playground" 3 + description = "Try MLF in your browser" 4 + template = "playground.html" 5 + +++ 6 + 7 + # MLF Playground 8 + 9 + Try MLF directly in your browser. The editor uses WebAssembly to provide instant feedback.
+22
website/justfile
··· 1 + # Build development version (faster, no optimizations) 2 + build-dev: 3 + #!/usr/bin/env bash 4 + cd .. 5 + wasm-pack build mlf-wasm --target web --out-dir ../website/static/js/pkg 6 + cd website 7 + zola build 8 + 9 + # Build release version (optimized) 10 + build-release: 11 + #!/usr/bin/env bash 12 + cd .. 13 + wasm-pack build mlf-wasm --target web --out-dir ../website/static/js/pkg --release 14 + cd website 15 + if command -v wasm-opt >/dev/null 2>&1; then 16 + wasm-opt -Oz static/js/pkg/mlf_wasm_bg.wasm -o static/js/pkg/mlf_wasm_bg.wasm 17 + fi 18 + zola build 19 + 20 + # Start development server with live reload 21 + serve: 22 + zola serve
+838
website/sass/style.scss
··· 1 + /* Dark, colorful theme */ 2 + 3 + :root { 4 + /* Primary palette */ 5 + --dark-spring-green: #1b724a; 6 + --english-violet: #4c4b63; 7 + --silver: #c3c3c3; 8 + --french-rose: #f44174; 9 + --tigers-eye: #b1740f; 10 + 11 + /* Theme application */ 12 + --accent: #1b724a; 13 + --accent-light: #2a9966; 14 + --accent-hover: #248558; 15 + --secondary: #f44174; 16 + --tertiary: #b1740f; 17 + --text: #e8e8ea; 18 + --text-light: #c3c3c3; 19 + --text-muted: #9999a0; 20 + --bg: #2a2935; 21 + --bg-alt: #363545; 22 + --bg-elevated: #4c4b63; 23 + --border: #555465; 24 + --code-bg: #1f1e28; 25 + --code-text: #e8e8ea; 26 + --success: #1b724a; 27 + --error: #f44174; 28 + --warning: #b1740f; 29 + } 30 + 31 + * { 32 + margin: 0; 33 + padding: 0; 34 + box-sizing: border-box; 35 + } 36 + 37 + html { 38 + height: 100%; 39 + } 40 + 41 + body { 42 + font-family: 'Atkinson Hyperlegible', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif; 43 + line-height: 1.7; 44 + color: var(--text); 45 + background: var(--bg); 46 + font-size: 16px; 47 + min-height: 100%; 48 + display: flex; 49 + flex-direction: column; 50 + } 51 + 52 + main { 53 + flex: 1; 54 + } 55 + 56 + .container { 57 + max-width: 980px; 58 + margin: 0 auto; 59 + padding: 0 1.5rem; 60 + } 61 + 62 + /* Typography */ 63 + h1, h2, h3, h4 { 64 + font-weight: 600; 65 + line-height: 1.3; 66 + margin-bottom: 1rem; 67 + } 68 + 69 + h1 { 70 + font-size: 2.5rem; 71 + letter-spacing: -0.02em; 72 + } 73 + 74 + h2 { 75 + font-size: 2rem; 76 + letter-spacing: -0.01em; 77 + } 78 + 79 + h3 { 80 + font-size: 1.5rem; 81 + } 82 + 83 + a { 84 + color: var(--accent); 85 + text-decoration: none; 86 + } 87 + 88 + a:hover { 89 + text-decoration: underline; 90 + } 91 + 92 + /* Header */ 93 + header { 94 + background: var(--bg-elevated); 95 + border-bottom: 1px solid var(--border); 96 + } 97 + 98 + nav { 99 + padding: 1rem 0; 100 + } 101 + 102 + .nav-container { 103 + max-width: 980px; 104 + margin: 0 auto; 105 + padding: 0 1.5rem; 106 + display: flex; 107 + justify-content: space-between; 108 + align-items: center; 109 + } 110 + 111 + .warning-banner { 112 + background: repeating-linear-gradient( 113 + 45deg, 114 + #ffcc00, 115 + #ffcc00 20px, 116 + #000000 20px, 117 + #000000 40px 118 + ); 119 + padding: 0.5rem 0; 120 + text-align: center; 121 + } 122 + 123 + .warning-content { 124 + color: #ffffff; 125 + font-weight: 700; 126 + font-size: 0.875rem; 127 + text-transform: uppercase; 128 + letter-spacing: 0.05em; 129 + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); 130 + } 131 + 132 + .logo { 133 + font-size: 1.5rem; 134 + font-weight: 700; 135 + color: var(--accent); 136 + } 137 + 138 + .nav-links { 139 + display: flex; 140 + gap: 2rem; 141 + list-style: none; 142 + } 143 + 144 + .nav-links a { 145 + color: var(--text); 146 + font-weight: 500; 147 + } 148 + 149 + .nav-links a:hover { 150 + color: var(--accent); 151 + text-decoration: none; 152 + } 153 + 154 + .nav-links a[target="_blank"]::after { 155 + content: "↗"; 156 + display: inline-block; 157 + margin-left: 0.25rem; 158 + font-size: 0.875em; 159 + vertical-align: super; 160 + line-height: 0; 161 + } 162 + 163 + /* Hero */ 164 + .hero { 165 + padding: 4rem 0 3rem; 166 + text-align: center; 167 + } 168 + 169 + .hero h1 { 170 + font-size: 3rem; 171 + margin-bottom: 0.5rem; 172 + color: var(--text); 173 + } 174 + 175 + .tagline { 176 + font-size: 1.25rem; 177 + color: var(--text-light); 178 + margin-bottom: 2rem; 179 + } 180 + 181 + .cta-buttons { 182 + display: flex; 183 + gap: 1rem; 184 + justify-content: center; 185 + margin-bottom: 3rem; 186 + } 187 + 188 + .btn { 189 + display: inline-block; 190 + padding: 0.625rem 1.25rem; 191 + border-radius: 0.25rem; 192 + font-weight: 600; 193 + border: 2px solid var(--accent); 194 + transition: all 0.2s; 195 + cursor: pointer; 196 + font-size: 1rem; 197 + } 198 + 199 + .btn-primary { 200 + background: var(--accent); 201 + color: white; 202 + } 203 + 204 + .btn-primary:hover { 205 + background: var(--accent-hover); 206 + border-color: var(--accent-hover); 207 + text-decoration: none; 208 + } 209 + 210 + .btn-secondary { 211 + background: transparent; 212 + color: var(--secondary); 213 + border-color: var(--secondary); 214 + } 215 + 216 + .btn-secondary:hover { 217 + background: var(--secondary); 218 + color: white; 219 + border-color: var(--secondary); 220 + text-decoration: none; 221 + } 222 + 223 + .btn-small { 224 + padding: 0.5rem 1rem; 225 + font-size: 0.875rem; 226 + } 227 + 228 + /* Example - side by side like toml.io */ 229 + .example { 230 + padding: 3rem 0; 231 + } 232 + 233 + .example h2 { 234 + text-align: center; 235 + margin-bottom: 2.5rem; 236 + color: var(--secondary); 237 + } 238 + 239 + .code-comparison { 240 + display: grid; 241 + grid-template-columns: 1fr 1fr; 242 + gap: 2rem; 243 + margin-bottom: 1rem; 244 + align-items: stretch; 245 + } 246 + 247 + .code-block { 248 + background: var(--code-bg); 249 + border-radius: 0.375rem; 250 + overflow: hidden; 251 + border: 1px solid var(--border); 252 + display: flex; 253 + flex-direction: column; 254 + height: 500px; 255 + } 256 + 257 + .code-block h3 { 258 + padding: 0.75rem 1rem; 259 + background: var(--bg-elevated); 260 + color: var(--accent-light); 261 + font-size: 0.875rem; 262 + font-weight: 600; 263 + border-bottom: 1px solid var(--border); 264 + flex-shrink: 0; 265 + } 266 + 267 + .code-block pre { 268 + margin: 0; 269 + padding: 0.75rem 1rem; 270 + overflow-x: auto; 271 + } 272 + 273 + .code-block code { 274 + font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace; 275 + font-size: 0.875rem; 276 + color: var(--code-text); 277 + line-height: 1.6; 278 + } 279 + 280 + .code-content { 281 + overflow-y: auto; 282 + flex: 1; 283 + min-height: 0; 284 + } 285 + 286 + .code-content > * { 287 + margin: 0; 288 + } 289 + 290 + .code-content pre { 291 + margin: 0; 292 + height: 100%; 293 + background-color: transparent !important; 294 + } 295 + 296 + /* Features - simple list like toml.io */ 297 + .features { 298 + padding: 3rem 0; 299 + background: var(--bg-alt); 300 + } 301 + 302 + .features h2 { 303 + text-align: center; 304 + margin-bottom: 2.5rem; 305 + color: var(--text); 306 + } 307 + 308 + .feature-list { 309 + max-width: 720px; 310 + margin: 0 auto; 311 + } 312 + 313 + .feature { 314 + padding: 1.5rem 0; 315 + border-bottom: 1px solid var(--border); 316 + } 317 + 318 + .feature:last-child { 319 + border-bottom: none; 320 + } 321 + 322 + .feature h3 { 323 + font-size: 1.25rem; 324 + margin-bottom: 0.5rem; 325 + color: var(--accent-light); 326 + } 327 + 328 + .feature p { 329 + color: var(--text-light); 330 + } 331 + 332 + /* Playground */ 333 + .playground { 334 + padding: 3rem 0; 335 + } 336 + 337 + .playground-page { 338 + padding: 0.5rem; 339 + height: calc(100vh - 81px); /* header height + border */ 340 + display: flex; 341 + flex-direction: column; 342 + } 343 + 344 + /* Hide footer on playground page */ 345 + body:has(.playground-page) footer { 346 + display: none; 347 + } 348 + 349 + .playground-page .lead { 350 + font-size: 1.25rem; 351 + color: var(--text-light); 352 + margin-bottom: 2rem; 353 + text-align: center; 354 + } 355 + 356 + .playground h2 { 357 + text-align: center; 358 + margin-bottom: 2.5rem; 359 + color: var(--accent); 360 + } 361 + 362 + .playground-container { 363 + display: grid; 364 + grid-template-columns: 1fr 1fr; 365 + gap: 0.5rem; 366 + height: 100%; 367 + width: 100%; 368 + } 369 + 370 + .editor-panel, .output-panel { 371 + border: 1px solid var(--border); 372 + overflow: hidden; 373 + display: flex; 374 + flex-direction: column; 375 + } 376 + 377 + .panel-header { 378 + display: flex; 379 + justify-content: space-between; 380 + align-items: center; 381 + padding: 0.75rem 1rem; 382 + background: var(--bg-alt); 383 + border-bottom: 1px solid var(--border); 384 + height: 3rem; 385 + } 386 + 387 + .panel-header h3 { 388 + font-size: 0.875rem; 389 + font-weight: 600; 390 + margin: 0; 391 + } 392 + 393 + .tabs { 394 + display: flex; 395 + gap: 0.5rem; 396 + } 397 + 398 + .tab { 399 + padding: 0.375rem 0.75rem; 400 + background: transparent; 401 + border: 1px solid var(--border); 402 + border-radius: 0.25rem; 403 + cursor: pointer; 404 + font-size: 0.813rem; 405 + color: var(--text-light); 406 + transition: all 0.2s; 407 + } 408 + 409 + .tab:hover { 410 + color: var(--text); 411 + border-color: var(--text-light); 412 + } 413 + 414 + .tab.active { 415 + background: var(--accent); 416 + color: white; 417 + border-color: var(--accent); 418 + } 419 + 420 + textarea { 421 + width: 100%; 422 + flex: 1; 423 + min-height: 0; 424 + padding: 1rem; 425 + border: none; 426 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace; 427 + font-size: 0.875rem; 428 + line-height: 1.6; 429 + resize: none; 430 + background: var(--code-bg); 431 + color: var(--code-text); 432 + } 433 + 434 + textarea:focus { 435 + outline: none; 436 + } 437 + 438 + textarea[readonly] { 439 + opacity: 0.9; 440 + } 441 + 442 + textarea::placeholder { 443 + color: #718096; 444 + } 445 + 446 + .shiki-editor-container { 447 + width: 100%; 448 + flex: 1; 449 + min-height: 0; 450 + overflow: auto; 451 + background: var(--code-bg); 452 + } 453 + 454 + .shiki-editor { 455 + min-height: 100%; 456 + padding: 1rem; 457 + border: none; 458 + font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace; 459 + font-size: 0.875rem; 460 + line-height: 1.6; 461 + color: var(--code-text); 462 + white-space: pre; 463 + tab-size: 4; 464 + caret-color: var(--text); 465 + } 466 + 467 + .shiki-editor:focus { 468 + outline: none; 469 + } 470 + 471 + .shiki-editor span { 472 + font-family: inherit; 473 + font-size: inherit; 474 + line-height: inherit; 475 + } 476 + 477 + .shiki-output-container { 478 + width: 100%; 479 + flex: 1; 480 + min-height: 0; 481 + overflow: auto; 482 + background: var(--code-bg); 483 + padding: 1rem; 484 + font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace; 485 + font-size: 0.875rem; 486 + line-height: 1.6; 487 + color: var(--code-text); 488 + white-space: pre; 489 + } 490 + 491 + .shiki-output-container span { 492 + font-family: inherit; 493 + font-size: inherit; 494 + line-height: inherit; 495 + } 496 + 497 + .output-content { 498 + display: none; 499 + flex: 1; 500 + min-height: 0; 501 + overflow: auto; 502 + } 503 + 504 + .output-content.active { 505 + display: flex; 506 + flex-direction: column; 507 + } 508 + 509 + #validate-output { 510 + padding: 1rem; 511 + } 512 + 513 + #record-input { 514 + flex: 1; 515 + min-height: 0; 516 + margin-bottom: 1rem; 517 + } 518 + 519 + #validate-result { 520 + margin-top: 1rem; 521 + padding: 1rem; 522 + border-radius: 0.25rem; 523 + font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; 524 + font-size: 0.875rem; 525 + display: none; 526 + } 527 + 528 + #validate-result.success { 529 + background: #f0fff4; 530 + color: var(--success); 531 + border: 1px solid #9ae6b4; 532 + } 533 + 534 + #validate-result.error { 535 + background: #fff5f5; 536 + color: var(--error); 537 + border: 1px solid #feb2b2; 538 + } 539 + 540 + .errors { 541 + position: fixed; 542 + bottom: 0; 543 + left: 0; 544 + right: 0; 545 + max-height: 200px; 546 + overflow: auto; 547 + padding: 1rem; 548 + background: #fff5f5; 549 + border-top: 2px solid #feb2b2; 550 + color: var(--error); 551 + font-size: 0.875rem; 552 + display: none; 553 + white-space: pre-wrap; 554 + font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; 555 + z-index: 100; 556 + } 557 + 558 + .errors.show { 559 + display: block; 560 + } 561 + 562 + /* Docs section */ 563 + .docs { 564 + padding: 3rem 0; 565 + background: var(--bg-alt); 566 + } 567 + 568 + .docs h2 { 569 + text-align: center; 570 + margin-bottom: 2.5rem; 571 + color: var(--tertiary); 572 + } 573 + 574 + .docs-list { 575 + max-width: 720px; 576 + margin: 0 auto; 577 + } 578 + 579 + .doc-item { 580 + display: block; 581 + padding: 1.5rem; 582 + margin-bottom: 1rem; 583 + background: var(--bg); 584 + border: 1px solid var(--border); 585 + border-radius: 0.375rem; 586 + transition: border-color 0.2s; 587 + } 588 + 589 + .doc-item:hover { 590 + border-color: var(--accent-light); 591 + background: var(--bg-elevated); 592 + } 593 + 594 + .doc-item h3 { 595 + font-size: 1.25rem; 596 + color: var(--accent-light); 597 + margin-bottom: 0.5rem; 598 + } 599 + 600 + .doc-item p { 601 + color: var(--text-light); 602 + margin: 0; 603 + } 604 + 605 + /* Footer */ 606 + footer { 607 + padding: 2rem 0; 608 + text-align: center; 609 + color: var(--text-light); 610 + border-top: 1px solid var(--border); 611 + font-size: 0.875rem; 612 + } 613 + 614 + /* Responsive */ 615 + @media (max-width: 768px) { 616 + .hero h1 { 617 + font-size: 2rem; 618 + } 619 + 620 + .tagline { 621 + font-size: 1.125rem; 622 + } 623 + 624 + .code-comparison { 625 + grid-template-columns: 1fr; 626 + gap: 1rem; 627 + } 628 + 629 + .playground-container { 630 + grid-template-columns: 1fr; 631 + } 632 + 633 + .nav-links { 634 + gap: 1rem; 635 + font-size: 0.875rem; 636 + } 637 + 638 + .cta-buttons { 639 + flex-direction: column; 640 + align-items: center; 641 + } 642 + 643 + .btn { 644 + width: 100%; 645 + max-width: 200px; 646 + text-align: center; 647 + } 648 + 649 + .doc-layout { 650 + grid-template-columns: 1fr; 651 + gap: 2rem; 652 + } 653 + 654 + .doc-sidebar { 655 + position: static; 656 + border-bottom: 1px solid var(--border); 657 + padding-bottom: 1.5rem; 658 + margin-bottom: 1.5rem; 659 + } 660 + 661 + .doc-nav ul { 662 + display: flex; 663 + flex-wrap: wrap; 664 + gap: 0.5rem; 665 + } 666 + 667 + .doc-nav li { 668 + margin-bottom: 0; 669 + } 670 + } 671 + 672 + @media (max-width: 480px) { 673 + .container { 674 + padding: 0 1rem; 675 + } 676 + 677 + .hero { 678 + padding: 2rem 0; 679 + } 680 + 681 + .hero h1 { 682 + font-size: 1.75rem; 683 + } 684 + 685 + .nav-links { 686 + gap: 0.75rem; 687 + } 688 + 689 + textarea { 690 + height: 300px; 691 + } 692 + } 693 + 694 + /* Documentation pages */ 695 + .doc-page { 696 + padding: 3rem 0; 697 + } 698 + 699 + .doc-page .lead { 700 + font-size: 1.25rem; 701 + color: var(--text-light); 702 + margin-bottom: 2rem; 703 + } 704 + 705 + /* Documentation layout with sidebar */ 706 + .doc-layout { 707 + display: grid; 708 + grid-template-columns: 250px 1fr; 709 + gap: 3rem; 710 + align-items: start; 711 + } 712 + 713 + .doc-sidebar { 714 + position: sticky; 715 + top: 2rem; 716 + } 717 + 718 + .doc-nav h3 { 719 + font-size: 0.875rem; 720 + text-transform: uppercase; 721 + letter-spacing: 0.05em; 722 + color: var(--text-light); 723 + margin-bottom: 1rem; 724 + } 725 + 726 + .doc-nav ul { 727 + list-style: none; 728 + margin: 0; 729 + padding: 0; 730 + } 731 + 732 + .doc-nav li { 733 + margin-bottom: 0.5rem; 734 + } 735 + 736 + .doc-nav a { 737 + display: block; 738 + padding: 0.5rem 0.75rem; 739 + color: var(--text); 740 + border-radius: 0.25rem; 741 + transition: all 0.2s; 742 + font-size: 0.938rem; 743 + } 744 + 745 + .doc-nav a:hover { 746 + background: var(--bg-alt); 747 + color: var(--accent); 748 + text-decoration: none; 749 + } 750 + 751 + .doc-nav a.active { 752 + background: var(--accent); 753 + color: white; 754 + font-weight: 500; 755 + } 756 + 757 + .doc-main { 758 + min-width: 0; 759 + } 760 + 761 + .doc-content { 762 + max-width: 720px; 763 + } 764 + 765 + .doc-content h1 { 766 + margin-top: 2.5rem; 767 + margin-bottom: 1rem; 768 + } 769 + 770 + .doc-content h2 { 771 + margin-top: 2rem; 772 + margin-bottom: 0.75rem; 773 + padding-bottom: 0.25rem; 774 + border-bottom: 1px solid var(--border); 775 + } 776 + 777 + .doc-content h3 { 778 + margin-top: 1.5rem; 779 + margin-bottom: 0.5rem; 780 + } 781 + 782 + .doc-content p { 783 + margin-bottom: 1rem; 784 + } 785 + 786 + .doc-content ul, .doc-content ol { 787 + margin-bottom: 1rem; 788 + margin-left: 2rem; 789 + } 790 + 791 + .doc-content li { 792 + margin-bottom: 0.5rem; 793 + } 794 + 795 + .doc-content pre { 796 + margin: 1.5rem 0; 797 + padding: 1rem; 798 + background: var(--code-bg); 799 + border-radius: 0.375rem; 800 + overflow-x: auto; 801 + } 802 + 803 + .doc-content code { 804 + font-family: 'Atkinson Hyperlegible Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Menlo', monospace; 805 + font-size: 0.875rem; 806 + background: var(--code-bg); 807 + color: var(--code-text); 808 + padding: 0.125rem 0.375rem; 809 + border-radius: 0.25rem; 810 + } 811 + 812 + .doc-content pre code { 813 + padding: 0; 814 + background: none; 815 + } 816 + 817 + .doc-content strong { 818 + font-weight: 600; 819 + color: var(--text); 820 + } 821 + 822 + .doc-content hr { 823 + margin: 2rem 0; 824 + border: none; 825 + border-top: 1px solid var(--border); 826 + } 827 + 828 + .doc-content blockquote { 829 + margin: 1.5rem 0; 830 + padding-left: 1rem; 831 + border-left: 3px solid var(--accent); 832 + color: var(--text-light); 833 + } 834 + 835 + .logo a { 836 + color: var(--accent); 837 + text-decoration: none; 838 + }
+437
website/static/js/app.js
··· 1 + // Import Shiki from CDN 2 + import { createHighlighter } from 'https://esm.sh/shiki@1.22.2'; 3 + 4 + // WASM module 5 + let wasm; 6 + let highlighter; 7 + let editorContainer; 8 + 9 + // MLF language grammar 10 + const mlfGrammar = { 11 + name: 'mlf', 12 + scopeName: 'source.mlf', 13 + patterns: [ 14 + { include: '#comments' }, 15 + { include: '#keywords' }, 16 + { include: '#types' }, 17 + { include: '#strings' }, 18 + { include: '#numbers' }, 19 + { include: '#operators' } 20 + ], 21 + repository: { 22 + comments: { 23 + patterns: [ 24 + { 25 + name: 'comment.line.documentation.mlf', 26 + match: '///.*$' 27 + }, 28 + { 29 + name: 'comment.line.double-slash.mlf', 30 + match: '//.*$' 31 + } 32 + ] 33 + }, 34 + keywords: { 35 + patterns: [ 36 + { 37 + name: 'keyword.control.mlf', 38 + match: '\\b(namespace|use|record|alias|token|query|procedure|subscription|throws|constrained)\\b' 39 + }, 40 + { 41 + name: 'keyword.other.mlf', 42 + match: '\\b(main)\\b' 43 + } 44 + ] 45 + }, 46 + types: { 47 + patterns: [ 48 + { 49 + name: 'storage.type.builtin.mlf', 50 + match: '\\b(null|boolean|integer|number|string|bytes|blob|unknown)\\b' 51 + }, 52 + { 53 + name: 'storage.type.format.mlf', 54 + match: '\\b(Datetime|Uri|AtUri|Did|Handle|Nsid|Cid|AtIdentifier|Language|Tid|RecordKey)\\b' 55 + }, 56 + { 57 + name: 'entity.name.type.mlf', 58 + match: '\\b[A-Z][a-zA-Z0-9_]*\\b' 59 + } 60 + ] 61 + }, 62 + strings: { 63 + patterns: [ 64 + { 65 + name: 'string.quoted.double.mlf', 66 + begin: '"', 67 + end: '"', 68 + patterns: [ 69 + { 70 + name: 'constant.character.escape.mlf', 71 + match: '\\\\.' 72 + } 73 + ] 74 + } 75 + ] 76 + }, 77 + numbers: { 78 + patterns: [ 79 + { 80 + name: 'constant.numeric.integer.mlf', 81 + match: '\\b\\d+\\b' 82 + } 83 + ] 84 + }, 85 + operators: { 86 + patterns: [ 87 + { 88 + name: 'punctuation.section.mlf', 89 + match: '[{}()\\[\\]]' 90 + }, 91 + { 92 + name: 'punctuation.separator.mlf', 93 + match: '[,;:]' 94 + }, 95 + { 96 + name: 'keyword.operator.optional.mlf', 97 + match: '\\?' 98 + }, 99 + { 100 + name: 'keyword.operator.union.mlf', 101 + match: '\\|' 102 + }, 103 + { 104 + name: 'keyword.operator.assignment.mlf', 105 + match: '=' 106 + }, 107 + { 108 + name: 'keyword.operator.arrow.mlf', 109 + match: '->' 110 + } 111 + ] 112 + } 113 + } 114 + }; 115 + 116 + // Load WASM module and Shiki 117 + async function init() { 118 + try { 119 + // Load WASM 120 + const wasmModule = await import('./pkg/mlf_wasm.js'); 121 + await wasmModule.default(); 122 + wasm = wasmModule; 123 + 124 + // Load Shiki with custom MLF grammar and JSON 125 + highlighter = await createHighlighter({ 126 + themes: ['dracula'], 127 + langs: ['json', mlfGrammar] 128 + }); 129 + 130 + // Initialize the editor 131 + initEditor(); 132 + setupEventListeners(); 133 + 134 + // Generate initial output 135 + handleCheck(); 136 + } catch (error) { 137 + console.error('Failed to initialize:', error); 138 + hidePlayground(); 139 + showError('Failed to load playground. Error: ' + error.message); 140 + } 141 + } 142 + 143 + function initEditor() { 144 + const textarea = document.getElementById('mlf-editor'); 145 + const initialCode = textarea.value; 146 + 147 + // Create editor container 148 + editorContainer = document.createElement('div'); 149 + editorContainer.className = 'shiki-editor-container'; 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'; 156 + 157 + editorContainer.appendChild(editor); 158 + 159 + // Replace textarea with editor 160 + textarea.style.display = 'none'; 161 + textarea.parentNode.insertBefore(editorContainer, textarea); 162 + 163 + // Set initial content 164 + updateHighlighting(initialCode); 165 + 166 + // Convert JSON output textarea to highlighted div 167 + const jsonTextarea = document.getElementById('lexicon-result'); 168 + const jsonContainer = document.createElement('div'); 169 + jsonContainer.className = 'shiki-output-container'; 170 + jsonContainer.id = 'lexicon-result-container'; 171 + 172 + jsonTextarea.style.display = 'none'; 173 + jsonTextarea.parentNode.insertBefore(jsonContainer, jsonTextarea); 174 + } 175 + 176 + function updateHighlighting(code) { 177 + if (!highlighter || !editorContainer) return; 178 + 179 + const editor = editorContainer.querySelector('.shiki-editor'); 180 + if (!editor) return; 181 + 182 + // Store cursor position 183 + const cursorOffset = getCaretPosition(editor); 184 + 185 + // Update highlighted content 186 + const html = highlighter.codeToHtml(code, { 187 + lang: 'mlf', 188 + theme: 'dracula' 189 + }); 190 + 191 + // Extract just the code content (remove pre/code wrapper) 192 + const temp = document.createElement('div'); 193 + temp.innerHTML = html; 194 + const codeElement = temp.querySelector('code'); 195 + 196 + if (codeElement) { 197 + editor.innerHTML = codeElement.innerHTML; 198 + } else { 199 + editor.textContent = code; 200 + } 201 + 202 + // Restore cursor position 203 + if (document.activeElement === editor) { 204 + setCaretPosition(editor, cursorOffset); 205 + } 206 + } 207 + 208 + function getCaretPosition(element) { 209 + let caretOffset = 0; 210 + const selection = window.getSelection(); 211 + 212 + if (selection.rangeCount > 0) { 213 + const range = selection.getRangeAt(0); 214 + const preCaretRange = range.cloneRange(); 215 + preCaretRange.selectNodeContents(element); 216 + preCaretRange.setEnd(range.endContainer, range.endOffset); 217 + caretOffset = preCaretRange.toString().length; 218 + } 219 + 220 + return caretOffset; 221 + } 222 + 223 + function setCaretPosition(element, offset) { 224 + const range = document.createRange(); 225 + const selection = window.getSelection(); 226 + 227 + let charCount = 0; 228 + let found = false; 229 + 230 + function traverseNodes(node) { 231 + if (found) return; 232 + 233 + if (node.nodeType === Node.TEXT_NODE) { 234 + const nextCharCount = charCount + node.length; 235 + if (offset <= nextCharCount) { 236 + range.setStart(node, Math.min(offset - charCount, node.length)); 237 + range.collapse(true); 238 + found = true; 239 + return; 240 + } 241 + charCount = nextCharCount; 242 + } else { 243 + for (let i = 0; i < node.childNodes.length; i++) { 244 + traverseNodes(node.childNodes[i]); 245 + if (found) return; 246 + } 247 + } 248 + } 249 + 250 + traverseNodes(element); 251 + 252 + if (found) { 253 + selection.removeAllRanges(); 254 + selection.addRange(range); 255 + } 256 + } 257 + 258 + function getEditorContent() { 259 + const editor = editorContainer?.querySelector('.shiki-editor'); 260 + return editor ? editor.textContent : ''; 261 + } 262 + 263 + function updateJsonOutput(jsonString) { 264 + const container = document.getElementById('lexicon-result-container'); 265 + if (!container || !highlighter) return; 266 + 267 + // Format JSON 268 + try { 269 + const formatted = JSON.stringify(JSON.parse(jsonString), null, 2); 270 + 271 + // Highlight with Shiki 272 + const html = highlighter.codeToHtml(formatted, { 273 + lang: 'json', 274 + theme: 'dracula' 275 + }); 276 + 277 + // Extract code content 278 + const temp = document.createElement('div'); 279 + temp.innerHTML = html; 280 + const codeElement = temp.querySelector('code'); 281 + 282 + if (codeElement) { 283 + container.innerHTML = codeElement.innerHTML; 284 + } else { 285 + container.textContent = formatted; 286 + } 287 + } catch (e) { 288 + container.textContent = jsonString; 289 + } 290 + } 291 + 292 + function hidePlayground() { 293 + const playground = document.querySelector('.playground-container'); 294 + if (playground) { 295 + playground.style.display = 'none'; 296 + } 297 + } 298 + 299 + function setupEventListeners() { 300 + // Tab switching 301 + const tabs = document.querySelectorAll('.tab'); 302 + tabs.forEach(tab => { 303 + tab.addEventListener('click', () => { 304 + const tabName = tab.dataset.tab; 305 + switchTab(tabName); 306 + }); 307 + }); 308 + 309 + // 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); 318 + handleCheck(); 319 + }, 500); 320 + }); 321 + } 322 + 323 + // Auto-validate on record input change (debounced) 324 + const recordInput = document.getElementById('record-input'); 325 + if (recordInput) { 326 + let timeout; 327 + recordInput.addEventListener('input', () => { 328 + clearTimeout(timeout); 329 + timeout = setTimeout(handleValidate, 500); 330 + }); 331 + } 332 + } 333 + 334 + function switchTab(tabName) { 335 + // Update tab buttons 336 + document.querySelectorAll('.tab').forEach(tab => { 337 + tab.classList.toggle('active', tab.dataset.tab === tabName); 338 + }); 339 + 340 + // Update content 341 + document.querySelectorAll('.output-content').forEach(content => { 342 + content.classList.toggle('active', content.id === `${tabName}-output`); 343 + }); 344 + } 345 + 346 + function handleCheck() { 347 + if (!wasm) { 348 + return; 349 + } 350 + 351 + const source = getEditorContent(); 352 + 353 + try { 354 + // Check the MLF source 355 + const checkResult = wasm.check(source); 356 + 357 + if (checkResult.success) { 358 + 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 + 364 + const generateResult = wasm.generate_lexicon(source, namespace); 365 + 366 + if (generateResult.success) { 367 + updateJsonOutput(generateResult.lexicon); 368 + } else { 369 + showError(generateResult.error || 'Failed to generate lexicon'); 370 + } 371 + } else { 372 + const errors = checkResult.errors || ['Unknown error']; 373 + showError(Array.isArray(errors) ? errors.join('\n') : errors); 374 + } 375 + } catch (error) { 376 + showError(`Error: ${error.message}`); 377 + } 378 + } 379 + 380 + function handleValidate() { 381 + if (!wasm) { 382 + return; 383 + } 384 + 385 + const lexiconSource = getEditorContent(); 386 + const recordJson = document.getElementById('record-input').value; 387 + 388 + if (!recordJson.trim()) { 389 + showValidateError('Please enter a JSON record to validate'); 390 + return; 391 + } 392 + 393 + try { 394 + const result = wasm.validate_record(lexiconSource, recordJson); 395 + 396 + if (result.success) { 397 + showValidateSuccess('✓ Record is valid'); 398 + } else { 399 + const errors = result.errors || ['Validation failed']; 400 + showValidateError(Array.isArray(errors) ? errors.join('\n') : errors); 401 + } 402 + } catch (error) { 403 + showValidateError(`Error: ${error.message}`); 404 + } 405 + } 406 + 407 + function showError(message) { 408 + const errorDiv = document.getElementById('errors'); 409 + errorDiv.textContent = message; 410 + errorDiv.classList.add('show'); 411 + } 412 + 413 + function hideError() { 414 + const errorDiv = document.getElementById('errors'); 415 + errorDiv.classList.remove('show'); 416 + } 417 + 418 + function showValidateSuccess(message) { 419 + const resultDiv = document.getElementById('validate-result'); 420 + resultDiv.textContent = message; 421 + resultDiv.className = 'success'; 422 + resultDiv.style.display = 'block'; 423 + } 424 + 425 + function showValidateError(message) { 426 + const resultDiv = document.getElementById('validate-result'); 427 + resultDiv.textContent = message; 428 + resultDiv.className = 'error'; 429 + resultDiv.style.display = 'block'; 430 + } 431 + 432 + // Initialize when DOM is ready 433 + if (document.readyState === 'loading') { 434 + document.addEventListener('DOMContentLoaded', init); 435 + } else { 436 + init(); 437 + }
+17
website/static/logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 40" fill="none"> 2 + <style> 3 + @import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@700&amp;display=swap'); 4 + </style> 5 + 6 + <!-- Left brace --> 7 + <path d="M 10 8 C 6 8 4 10 4 14 L 4 17 C 4 19 3 20 1 20 C 3 20 4 21 4 23 L 4 26 C 4 30 6 32 10 32" 8 + stroke="#1b724a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/> 9 + 10 + <!-- MLF text --> 11 + <text x="34" y="28" font-family="'Atkinson Hyperlegible', system-ui, sans-serif" font-size="22" font-weight="700" 12 + fill="#c3c3c3" text-anchor="middle">MLF</text> 13 + 14 + <!-- Right brace --> 15 + <path d="M 58 8 C 62 8 64 10 64 14 L 64 17 C 64 19 65 20 67 20 C 65 20 64 21 64 23 L 64 26 C 64 30 62 32 58 32" 16 + stroke="#1b724a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/> 17 + </svg>
+77
website/syntaxes/mlf.sublime-syntax
··· 1 + %YAML 1.2 2 + --- 3 + name: MLF 4 + file_extensions: 5 + - mlf 6 + scope: source.mlf 7 + 8 + contexts: 9 + main: 10 + - include: comments 11 + - include: keywords 12 + - include: types 13 + - include: strings 14 + - include: numbers 15 + - include: operators 16 + 17 + comments: 18 + - match: '///' 19 + scope: punctuation.definition.comment.mlf 20 + push: 21 + - meta_scope: comment.line.documentation.mlf 22 + - match: $ 23 + pop: true 24 + - match: '//' 25 + scope: punctuation.definition.comment.mlf 26 + push: 27 + - meta_scope: comment.line.double-slash.mlf 28 + - match: $ 29 + pop: true 30 + 31 + keywords: 32 + - match: '\b(namespace|use|record|alias|token|query|procedure|subscription|throws|constrained)\b' 33 + scope: keyword.control.mlf 34 + - match: '\b(main)\b' 35 + scope: keyword.other.mlf 36 + 37 + types: 38 + # Primitive types 39 + - match: '\b(null|boolean|integer|number|string|bytes|blob|unknown)\b' 40 + scope: storage.type.builtin.mlf 41 + 42 + # Format types 43 + - match: '\b(Datetime|Uri|AtUri|Did|Handle|Nsid|Cid|AtIdentifier|Language|Tid|RecordKey)\b' 44 + scope: storage.type.format.mlf 45 + 46 + # User-defined types (capitalized identifiers) 47 + - match: '\b[A-Z][a-zA-Z0-9_]*\b' 48 + scope: entity.name.type.mlf 49 + 50 + strings: 51 + - match: '"' 52 + scope: punctuation.definition.string.begin.mlf 53 + push: 54 + - meta_scope: string.quoted.double.mlf 55 + - match: '\\.' 56 + scope: constant.character.escape.mlf 57 + - match: '"' 58 + scope: punctuation.definition.string.end.mlf 59 + pop: true 60 + 61 + numbers: 62 + - match: '\b\d+\b' 63 + scope: constant.numeric.integer.mlf 64 + 65 + operators: 66 + - match: '[{}()\[\]]' 67 + scope: punctuation.section.mlf 68 + - match: '[,;:]' 69 + scope: punctuation.separator.mlf 70 + - match: '\?' 71 + scope: keyword.operator.optional.mlf 72 + - match: '\|' 73 + scope: keyword.operator.union.mlf 74 + - match: '=' 75 + scope: keyword.operator.assignment.mlf 76 + - match: '->' 77 + scope: keyword.operator.arrow.mlf
+51
website/templates/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="{{ lang | default(value='en') }}"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>{% block title %}{{ config.title }}{% endblock %}</title> 7 + <meta name="description" content="{% block description %}{{ config.description }}{% endblock %}"> 8 + <link rel="icon" type="image/svg+xml" href="{{ get_url(path='logo.svg') }}"> 9 + <link rel="preconnect" href="https://fonts.googleapis.com"> 10 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 11 + <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Atkinson+Hyperlegible+Mono:wght@400;700&display=swap" rel="stylesheet"> 12 + <link rel="stylesheet" href="{{ get_url(path='style.css') }}"> 13 + {% block extra_head %}{% endblock %} 14 + </head> 15 + <body> 16 + <header> 17 + <nav> 18 + <div class="nav-container"> 19 + <div class="logo"> 20 + <a href="{{ get_url(path='/') }}"> 21 + <img src="{{ get_url(path='logo.svg') }}" alt="MLF" height="48"> 22 + </a> 23 + </div> 24 + <ul class="nav-links"> 25 + <li><a href="{{ get_url(path='/') }}">Home</a></li> 26 + <li><a href="{{ get_url(path='/docs') }}">Docs</a></li> 27 + <li><a href="{{ get_url(path='@/playground.md') }}">Playground</a></li> 28 + <li><a href="{{ config.extra.github_url }}" target="_blank">Source</a></li> 29 + </ul> 30 + </div> 31 + </nav> 32 + <div class="warning-banner"> 33 + <div class="warning-content"> 34 + Pre-release: not everything will work as expected 35 + </div> 36 + </div> 37 + </header> 38 + 39 + <main> 40 + {% block content %}{% endblock %} 41 + </main> 42 + 43 + <footer> 44 + <div class="container"> 45 + <p>MLF is licensed under <a href="https://choosealicense.com/licenses/mit/" target="_blank">MIT</a></p> 46 + </div> 47 + </footer> 48 + 49 + {% block extra_scripts %}{% endblock %} 50 + </body> 51 + </html>
+58
website/templates/index.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <section id="home" class="hero"> 5 + <div class="container"> 6 + <h1>Matt's Lexicon Format</h1> 7 + <p class="tagline">A human-friendly DSL for ATProto Lexicons</p> 8 + <div class="cta-buttons"> 9 + <a href="{{ get_url(path='@/playground.md') }}" class="btn btn-primary">Try it now</a> 10 + <a href="{{ get_url(path='/docs') }}" class="btn btn-primary">Read the Docs</a> 11 + <a href="{{ config.extra.github_url }}" class="btn btn-secondary">View Source</a> 12 + </div> 13 + </div> 14 + </section> 15 + 16 + <section id="example" class="example"> 17 + <div class="container"> 18 + <div class="code-comparison"> 19 + <div class="code-block"> 20 + <h3>com.example.thread.mlf</h3> 21 + <div class="code-content"> 22 + {{ section.extra.mlf_example | markdown | safe }} 23 + </div> 24 + </div> 25 + <div class="code-block"> 26 + <h3>com/example/thread.json</h3> 27 + <div class="code-content"> 28 + {{ section.extra.json_example | markdown | safe }} 29 + </div> 30 + </div> 31 + </div> 32 + </div> 33 + </section> 34 + 35 + <section id="features" class="features"> 36 + <div class="container"> 37 + <h2>What is MLF?</h2> 38 + <div class="feature-list"> 39 + <div class="feature"> 40 + <h3>Human-Friendly Syntax</h3> 41 + <p>Clean, readable syntax inspired by TypeScript and Rust. Write lexicons the way you think about them, with full type safety and constraint validation.</p> 42 + </div> 43 + <div class="feature"> 44 + <h3>100% ATProto Fidelity</h3> 45 + <p>Bidirectional conversion with ATProto JSON Lexicons. Everything that can be expressed in JSON can be expressed in MLF, and vice versa.</p> 46 + </div> 47 + <div class="feature"> 48 + <h3>Rich Tooling</h3> 49 + <p>Command-line interface, WebAssembly library, tree-sitter grammar for editor integration (Neovim, Helix, VS Code, Emacs), and comprehensive error messages.</p> 50 + </div> 51 + <div class="feature"> 52 + <h3>Type-Safe</h3> 53 + <p>Compile-time type checking catches errors before they become runtime problems. Constrained types ensure your data meets specifications.</p> 54 + </div> 55 + </div> 56 + </div> 57 + </section> 58 + {% endblock %}
+49
website/templates/page.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block title %}{{ page.title }} - {{ config.title }}{% endblock %} 4 + 5 + {% block content %} 6 + <article class="doc-page"> 7 + <div class="container"> 8 + {% if page.components and page.components is containing("docs") %} 9 + <div class="doc-layout"> 10 + <aside class="doc-sidebar"> 11 + <nav class="doc-nav"> 12 + <h3>Documentation</h3> 13 + {% set docs_section = get_section(path="docs/_index.md") %} 14 + <ul> 15 + {% for p in docs_section.pages %} 16 + <li> 17 + <a href="{{ p.permalink }}" {% if p.permalink == page.permalink %}class="active"{% endif %}> 18 + {{ p.title }} 19 + </a> 20 + </li> 21 + {% endfor %} 22 + </ul> 23 + </nav> 24 + </aside> 25 + 26 + <div class="doc-main"> 27 + <h1>{{ page.title }}</h1> 28 + {% if page.description %} 29 + <p class="lead">{{ page.description }}</p> 30 + {% endif %} 31 + 32 + <div class="doc-content"> 33 + {{ page.content | safe }} 34 + </div> 35 + </div> 36 + </div> 37 + {% else %} 38 + <h1>{{ page.title }}</h1> 39 + {% if page.description %} 40 + <p class="lead">{{ page.description }}</p> 41 + {% endif %} 42 + 43 + <div class="doc-content"> 44 + {{ page.content | safe }} 45 + </div> 46 + {% endif %} 47 + </div> 48 + </article> 49 + {% endblock %}
+56
website/templates/playground.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <section class="playground-page"> 5 + <div class="playground-container"> 6 + <div class="editor-panel"> 7 + <div class="panel-header"> 8 + <h3>MLF Source</h3> 9 + </div> 10 + <textarea id="mlf-editor" spellcheck="false">/// A forum thread 11 + record thread { 12 + /// Thread title 13 + title: string constrained { 14 + maxLength: 200, 15 + minLength: 1, 16 + }, 17 + /// Thread body 18 + body: string constrained { 19 + maxLength: 10000, 20 + }, 21 + /// Thread creation timestamp 22 + createdAt: Datetime, 23 + };</textarea> 24 + </div> 25 + 26 + <div class="output-panel"> 27 + <div class="panel-header"> 28 + <h3>Output</h3> 29 + <div class="tabs"> 30 + <button class="tab active" data-tab="lexicon">Lexicon</button> 31 + <button class="tab" data-tab="validate">Validate</button> 32 + </div> 33 + </div> 34 + 35 + <div id="lexicon-output" class="output-content active"> 36 + <textarea id="lexicon-result" readonly spellcheck="false" placeholder="Generated JSON lexicon will appear here..."></textarea> 37 + </div> 38 + 39 + <div id="validate-output" class="output-content"> 40 + <textarea id="record-input" placeholder='Paste a JSON record to validate, e.g.: 41 + { 42 + "title": "Welcome to the forums!", 43 + "body": "This is my first thread.", 44 + "createdAt": "2024-01-15T10:30:00Z" 45 + }'></textarea> 46 + <div id="validate-result"></div> 47 + </div> 48 + </div> 49 + </div> 50 + <div id="errors" class="errors"></div> 51 + </section> 52 + {% endblock %} 53 + 54 + {% block extra_scripts %} 55 + <script type="module" src="{{ get_url(path='js/app.js') }}"></script> 56 + {% endblock %}
+62
website/templates/section.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block title %}{{ section.title }} - {{ config.title }}{% endblock %} 4 + 5 + {% block content %} 6 + <article class="doc-page"> 7 + <div class="container"> 8 + {% if section.path is containing("docs") %} 9 + <div class="doc-layout"> 10 + <aside class="doc-sidebar"> 11 + <nav class="doc-nav"> 12 + <h3>Documentation</h3> 13 + <ul> 14 + {% for page in section.pages %} 15 + <li> 16 + <a href="{{ page.permalink }}"> 17 + {{ page.title }} 18 + </a> 19 + </li> 20 + {% endfor %} 21 + </ul> 22 + </nav> 23 + </aside> 24 + 25 + <div class="doc-main"> 26 + <h1>{{ section.title }}</h1> 27 + {% if section.description %} 28 + <p class="lead">{{ section.description }}</p> 29 + {% endif %} 30 + 31 + {% if section.content %} 32 + <div class="doc-content"> 33 + {{ section.content | safe }} 34 + </div> 35 + {% endif %} 36 + </div> 37 + </div> 38 + {% else %} 39 + <h1>{{ section.title }}</h1> 40 + 41 + {% if section.pages %} 42 + <div class="docs-list"> 43 + {% for page in section.pages %} 44 + <a href="{{ page.permalink }}" class="doc-item"> 45 + <h3>{{ page.title }}</h3> 46 + {% if page.description %} 47 + <p>{{ page.description }}</p> 48 + {% endif %} 49 + </a> 50 + {% endfor %} 51 + </div> 52 + {% endif %} 53 + 54 + {% if section.content %} 55 + <div class="doc-content"> 56 + {{ section.content | safe }} 57 + </div> 58 + {% endif %} 59 + {% endif %} 60 + </div> 61 + </article> 62 + {% endblock %}