forked from nulang-org/nulang
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommands.rs
More file actions
1874 lines (1707 loc) · 78.3 KB
/
Copy pathcommands.rs
File metadata and controls
1874 lines (1707 loc) · 78.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! `nula` CLI subcommands: `new`, `init`, `build`, `build-wasm`, `test`, `run`,
//! `list`, `clean`, `add`, `remove`, `watch`, `publish`, `deploy`.
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::package::lockfile::{Lockfile, LOCKFILE_FILE};
use crate::package::manifest::{Dependency, DependencyDetail, Manifest, MANIFEST_FILE};
use crate::package::resolver::resolve;
use crate::types::{NuError, NuResult, Span};
use crate::registry::RegistryClient;
thread_local! {
/// Optional per-thread override for the package root, set by tests to
/// avoid mutating the process-global working directory. `set_current_dir`
/// in one test raced with unrelated parallel tests resolving `stdlib::*`
/// and example files relative to `current_dir()`, causing random
/// cross-test failures that vanished in isolation.
static PACKAGE_ROOT_OVERRIDE: std::cell::RefCell<Option<PathBuf>> =
std::cell::RefCell::new(None);
}
/// The base directory a `cmd_*` function operates in: a per-thread test
/// override if present (see `PACKAGE_ROOT_OVERRIDE`), else the process
/// working directory. Never mutates global CWD state.
fn package_root() -> NuResult<PathBuf> {
if let Some(dir) = PACKAGE_ROOT_OVERRIDE.with(|c| c.borrow().clone()) {
return Ok(dir);
}
std::env::current_dir().map_err(|e| NuError::PackageError {
msg: format!("cannot read current directory: {}", e),
span: Span::default(),
})
}
/// Dispatch a `nula` invocation (`args` excludes the leading `nula`).
pub fn run(args: &[String]) -> NuResult<()> {
match args.first().map(String::as_str) {
Some("new") => {
let mut template: Option<&str> = None;
let mut path_arg: Option<&str> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--template" => {
i += 1;
if i < args.len() {
template = Some(args[i].as_str());
}
}
other => {
if path_arg.is_some() {
return Err(NuError::PackageError {
msg: format!("unexpected argument '{}' for nula new", other),
span: Span::default(),
});
}
path_arg = Some(other);
}
}
i += 1;
}
cmd_new(path_arg, template)
}
Some("init") => cmd_init(),
Some("build") => cmd_build(),
Some("build-wasm") => cmd_build_wasm(),
Some("test") => {
let mut filter: Option<&str> = None;
let mut verbose = false;
let mut watch = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--filter" => {
i += 1;
if i < args.len() {
filter = Some(args[i].as_str());
}
}
"--verbose" | "-v" => verbose = true,
"--watch" | "-w" => watch = true,
other => {
return Err(NuError::PackageError {
msg: format!("unknown flag '{}' for nula test", other),
span: Span::default(),
});
}
}
i += 1;
}
if watch {
cmd_test_watch(filter, verbose)
} else {
cmd_test(filter, verbose)
}
}
Some("run") => {
if args.get(1).map(String::as_str) == Some("--watch") {
cmd_run_watch()
} else {
cmd_run()
}
}
Some("watch") => cmd_run_watch(),
Some("add") => {
let name = args.get(1);
let mut path: Option<String> = None;
let mut git: Option<String> = None;
let mut version: Option<String> = None;
let mut i = 2;
while i < args.len() {
match args[i].as_str() {
"--path" => {
i += 1;
if i < args.len() {
path = Some(args[i].clone());
}
}
"--git" => {
i += 1;
if i < args.len() {
git = Some(args[i].clone());
}
}
"--version" => {
i += 1;
if i < args.len() {
version = Some(args[i].clone());
}
}
other => {
return Err(NuError::PackageError {
msg: format!("unknown flag '{}' for nula add", other),
span: Span::default(),
});
}
}
i += 1;
}
cmd_add(name, path.as_deref(), git.as_deref(), version.as_deref())
}
Some("remove") => cmd_remove(args.get(1).map(String::as_str)),
Some("list") => cmd_list(),
Some("clean") => cmd_clean(),
Some("doc") => {
let open = args.get(1).map(String::as_str) == Some("--open");
cmd_doc(open)
}
Some("publish") => {
let mut registry_url: Option<String> = None;
let mut token: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--registry" => {
i += 1;
if i < args.len() { registry_url = Some(args[i].clone()); }
}
"--token" => {
i += 1;
if i < args.len() { token = Some(args[i].clone()); }
}
other => {
return Err(NuError::PackageError {
msg: format!("unknown flag '{}' for nula publish", other),
span: Span::default(),
});
}
}
i += 1;
}
cmd_publish(registry_url, token)
}
Some("deploy") => {
let mut token: Option<String> = None;
let mut url: Option<String> = None;
let mut wasm = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--token" => {
i += 1;
if i < args.len() { token = Some(args[i].clone()); }
}
"--url" => {
i += 1;
if i < args.len() { url = Some(args[i].clone()); }
}
"--wasm" => wasm = true,
other => return Err(NuError::PackageError {
msg: format!("unknown flag '{}' for nula deploy", other),
span: Span::default()
})
}
i += 1;
}
cmd_deploy(wasm, url, token)
}
Some(other) => Err(NuError::PackageError {
msg: format!(
"unknown nula subcommand '{}' (expected new, init, build, build-wasm, test, run, add, remove, publish, deploy, watch, doc, list, or clean)",
other
),
span: Span::default(),
}),
None => {
print_usage();
Ok(())
}
}
}
fn print_usage() {
println!("nula — the Nulang package manager");
println!();
println!("Usage: nulang nula <COMMAND>");
println!();
println!("Commands:");
println!(" new <path> [--template <name>]");
println!(" Scaffold a new package directory");
println!(" Templates: default, cli, lib, full");
println!(" init Scaffold a new package in the current directory");
println!(" build Build the package (type-check + .nbc artifact in .nula/dist/)");
println!(" build-wasm Build package to .wasm + .cwasm in .nula/dist/");
println!(" test [--filter <substr>] [--verbose|-v] [--watch|-w] Run .nula test files");
println!(" run Build and run the package entry point");
println!(" run --watch Build and re-run on source changes");
println!(" watch Alias for 'run --watch'");
println!(" add <name> Add a dependency to Nulang.toml");
println!(" remove <name> Remove a dependency from Nulang.toml");
println!(" publish Publish the package to a registry");
println!(" --registry <url> Registry URL (or set in Nulang.toml)");
println!(" --token <token> Auth token (or set NULA_TOKEN)");
println!(" deploy Build and deploy the package to Nulang Cloud");
println!(" --wasm Also bundle .wasm + .cwasm artifacts");
println!(" --url <url> Cloud API URL (or set NULANG_CLOUD_URL)");
println!(" --token <token> Auth token (or set NULANG_CLOUD_TOKEN)");
println!(" list List resolved dependencies from Nulang.lock");
println!(" clean Remove build artifacts (.nula/dist/)");
println!(" doc [--open] Generate Markdown API docs (docs/api.md)");
}
/// `nula new <name> [--template <name>]`: scaffold a package directory.
fn cmd_new(path_arg: Option<&str>, template: Option<&str>) -> NuResult<()> {
let path_str = path_arg.ok_or_else(|| NuError::PackageError {
msg: "nula new requires a package name or path".to_string(),
span: Span::default(),
})?;
let dir = PathBuf::from(path_str);
let name = dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| NuError::PackageError {
msg: format!("invalid path '{}' — cannot extract package name", path_str),
span: Span::default(),
})?;
validate_package_name(name)?;
if dir.exists() {
return Err(NuError::PackageError {
msg: format!("directory '{}' already exists", dir.display()),
span: Span::default(),
});
}
let tmpl = template.unwrap_or("default");
let valid = [
"default",
"cli",
"lib",
"full",
"distributed",
"ai-agent",
"web",
];
if !valid.contains(&tmpl) {
return Err(NuError::PackageError {
msg: format!(
"unknown template '{}' (available: {})",
tmpl,
valid.join(", ")
),
span: Span::default(),
});
}
scaffold_package(&dir, name, tmpl)?;
println!("Created package '{}' at '{}'", name, dir.display());
Ok(())
}
/// `nula init`: scaffold a package in the current directory.
fn cmd_init() -> NuResult<()> {
let dir = package_root()?;
let manifest_path = dir.join(MANIFEST_FILE);
if manifest_path.exists() {
return Err(NuError::PackageError {
msg: format!(
"{} already exists in {} — package is already initialized",
MANIFEST_FILE,
dir.display()
),
span: Span::default(),
});
}
let name = dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("nulang-project");
validate_package_name(name)?;
scaffold_package(&dir, name, "default")?;
// Write a basic .gitignore
let gitignore = dir.join(".gitignore");
if !gitignore.exists() {
let _ = std::fs::write(&gitignore, "# Nulang build artifacts\n*.nbc\n.nula/\n");
}
println!("Initialized package '{}' in '{}'", name, dir.display());
Ok(())
}
fn validate_package_name(name: &str) -> NuResult<()> {
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(NuError::PackageError {
msg: format!(
"invalid package name '{}' (use letters, digits, '-' or '_')",
name
),
span: Span::default(),
});
}
Ok(())
}
/// Write the `Nulang.toml` + template source files for a new package.
fn scaffold_package(dir: &Path, name: &str, template: &str) -> NuResult<()> {
std::fs::create_dir_all(dir).map_err(|e| NuError::PackageError {
msg: format!("cannot create {}: {}", dir.display(), e),
span: Span::default(),
})?;
let manifest_path = dir.join(MANIFEST_FILE);
std::fs::write(
&manifest_path,
format!(
"[package]\nname = \"{}\"\nversion = \"0.1.0\"\n\n[dependencies]\n",
name
),
)
.map_err(|e| NuError::PackageError {
msg: format!("cannot write {}: {}", manifest_path.display(), e),
span: Span::default(),
})?;
for (rel_path, content) in template_files(template) {
let dest = dir.join(rel_path);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| NuError::PackageError {
msg: format!("cannot create {}: {}", parent.display(), e),
span: Span::default(),
})?;
}
std::fs::write(&dest, content).map_err(|e| NuError::PackageError {
msg: format!("cannot write {}: {}", dest.display(), e),
span: Span::default(),
})?;
}
Ok(())
}
/// Return the list of (relative_path, content) for a named template.
fn template_files(name: &str) -> Vec<(&'static str, &'static str)> {
match name {
"default" => vec![(
"src/main.nula",
"fn main() {\n perform IO.print(\"Hello from Nulang!\")\n}\n",
)],
"cli" => vec![(
"src/main.nula",
"// CLI template — a starting point for command-line tools.\n//\n// This file demonstrates:\n// System.arg — reading command-line arguments (0-indexed)\n// Env.get — reading environment variables\n// FS.write — writing output to a file\n// IO.print — printing to stdout\n// == nil — checking whether a value is nil\n\n// ── Helper (defined before `main` so it is in scope) ───────────────────────\n// Print a friendly greeting. Extracted as a function so the main flow\n// stays readable.\nfn greet(name) {\n perform IO.print(\"Hello, \" + name + \"!\")\n}\n\n// ── Entry point ────────────────────────────────────────────────────────────\n\nfn main() {\n // ── 1. Read the target name from the command line ──────────────────────\n // System.arg(0) = program name; System.arg(1) = script path.\n // System.arg(2) is the first user argument.\n let given = perform System.arg(2)\n\n // ── 2. Fall back to the USER environment variable ─────────────────────\n // Env.get returns nil when the variable is not set.\n match given {\n nil => {\n let user = perform Env.get(\"USER\")\n match user {\n nil => {\n // ── 3. Hard-coded default when nothing else is available ──────\n // Both arg and env were nil — use \"World\" as a friendly fallback.\n let name = \"World\"\n // Print usage hint because the user didn't supply a name.\n let prog = perform System.arg(0)\n perform IO.print(\"Usage: \" + prog + \" <name>\")\n perform IO.print(\"\")\n greet(name)\n }\n _ => {\n // Env.get returned a value; use it.\n greet(user)\n }\n }\n }\n _ => {\n // System.arg(2) returned a value; use it.\n greet(given)\n }\n }\n\n // ── 4. Optional: log the greeting to a file ───────────────────────────\n // Pass --log <path> to write the greeting to a file.\n // This shows how to handle optional flags and do file output.\n let log_path = perform System.arg(3)\n match log_path {\n nil => unit,\n _ => {\n match log_path {\n \"--log\" => {\n let path = perform System.arg(4)\n match path {\n nil => {\n perform IO.print(\"--log requires a file path\")\n }\n _ => {\n // Build the log line with timestamp-like prefix.\n let ts = perform Env.get(\"NU_TIMESTAMP\")\n let prefix = match ts {\n nil => \"[nulang]\",\n _ => \"[\" + ts + \"]\"\n }\n let out_name = match given {\n nil => match perform Env.get(\"USER\") {\n nil => \"World\",\n u => u\n },\n n => n\n }\n let line = prefix + \" Greeted \" + out_name\n let wrote = perform FS.write(path, line)\n match wrote {\n nil => perform IO.print(\"Warning: could not write log to \" + path),\n _ => perform IO.print(\"Logged to \" + path)\n }\n }\n }\n }\n _ => unit\n }\n }\n }\n}\n",
)],
"lib" => vec![
(
"src/main.nula",
"// Entry point for a library package.\n//\n// Library packages export public functions from `src/lib.nula` for other\n// packages to depend on. The entry point is a trivial smoke test — replace\n// it with your own application logic.\n\nfn main() {\n perform IO.print(\"Library package ready.\")\n perform IO.print(\"Run `nula test` to verify the public API.\")\n}\n",
),
(
"src/lib.nula",
"/// Add two integers and return the sum.\npub fn add(a: Int, b: Int) -> Int {\n a + b\n}\n",
),
(
"tests/test_add.nula",
"// Test file for the library's `add` function.\n//\n// Each test file runs standalone via `nula test` — helper functions must\n// be defined in the test file itself (or imported when the module system\n// supports cross-file imports).\n\nfn add(a: Int, b: Int) -> Int {\n a + b\n}\n\nfn main() {\n perform Test.assert_eq(add(1, 2), 3)\n perform Test.assert_eq(add(-5, 5), 0)\n}\n",
),
],
"full" => vec![
(
"README.md",
"# {{name}}\n\nA Nulang project.\n\n## Structure\n\n- `src/main.nula` — entry point\n- `src/lib.nula` — library module\n- `tests/` — test files\n- `examples/` — standalone demos\n\n## Commands\n\n```\n# Build and type-check\nnula build\n\n# Run the entry point\nnula run\n\n# Run tests\nnula test\n\n# Run a demo\nnulang examples/demo.nula\n```\n\n## Dependencies\n\nAdd dependencies with `nula add <name>`.\n",
),
(
"src/lib.nula",
"// Library module — reusable functions shared across the project.\n//\n// Public functions (marked `pub`) can be imported by other files.\n// Use `///` doc comments to document public API surfaces.\n\n/// Return a greeting for the given name.\npub fn greet(name: String) -> String {\n \"Hello, \" + name + \"!\"\n}\n\n/// Add two integers together.\npub fn add(a: Int, b: Int) -> Int {\n a + b\n}\n\n/// Compute the factorial of n recursively.\npub fn factorial(n: Int) -> Int {\n if n <= 1 then 1\n else n * factorial(n - 1)\n}\n\n/// Return a friendly message describing the sign of a number.\npub fn describe_number(n: Int) -> String {\n if n > 0 then \"positive\"\n else if n < 0 then \"negative\"\n else \"zero\"\n}\n",
),
(
"src/main.nula",
"// Entry point for the application.\n// All application logic lives here; the build system type-checks this file.\n\n// ── Library functions (defined before `main` so they are in scope) ─────────\n\n/// Return a greeting for the given name.\nfn greet(name: String) -> String {\n \"Hello, \" + name + \"!\"\n}\n\n/// Return a friendly label for a number's sign.\nfn describe_number(n: Int) -> String {\n if n > 0 then \"positive\"\n else if n < 0 then \"negative\"\n else \"zero\"\n}\n\n// ── Entry point ────────────────────────────────────────────────────────────\n\nfn main() {\n // Read an optional count from the environment.\n let upto = perform Env.get(\"COUNT\")\n let n = match upto {\n nil => 10,\n _ => perform Int.parse(upto)\n }\n\n let msg = greet(\"Nulang\")\n perform IO.print(msg)\n\n // Demonstrate basic operations.\n let sum = n + 42\n perform IO.print(\"n + 42 = \" + perform Int.to_string(sum))\n\n let desc = describe_number(n)\n perform IO.print(\"n is \" + desc)\n}\n",
),
(
"tests/test_lib.nula",
"// Tests for the library functions.\n//\n// Each test file runs standalone via `nula test`.\n// Define helper functions before `main` so they are in scope.\n\n/// Return a greeting for the given name.\nfn greet(name: String) -> String {\n \"Hello, \" + name + \"!\"\n}\n\n/// Add two integers together.\nfn add(a: Int, b: Int) -> Int {\n a + b\n}\n\n/// Compute the factorial of n recursively.\nfn factorial(n: Int) -> Int {\n if n <= 1 then 1\n else n * factorial(n - 1)\n}\n\n/// Return a friendly message describing the sign of a number.\nfn describe_number(n: Int) -> String {\n if n > 0 then \"positive\"\n else if n < 0 then \"negative\"\n else \"zero\"\n}\n\nfn main() {\n perform Test.assert_eq(greet(\"World\"), \"Hello, World!\")\n perform Test.assert_eq(add(40, 2), 42)\n perform Test.assert_eq(factorial(5), 120)\n perform Test.assert_eq(describe_number(7), \"positive\")\n perform Test.assert_eq(describe_number(-3), \"negative\")\n perform Test.assert_eq(describe_number(0), \"zero\")\n}\n",
),
(
"examples/demo.nula",
"// Demo script — a small standalone example using the library.\n//\n// Run with: nulang examples/demo.nula\n\n/// Return a greeting for the given name.\nfn greet(name: String) -> String {\n \"Hello, \" + name + \"!\"\n}\n\n/// Add two integers together.\nfn add(a: Int, b: Int) -> Int {\n a + b\n}\n\n/// Compute the factorial of n recursively.\nfn factorial(n: Int) -> Int {\n if n <= 1 then 1\n else n * factorial(n - 1)\n}\n\n/// Return a friendly message describing the sign of a number.\nfn describe_number(n: Int) -> String {\n if n > 0 then \"positive\"\n else if n < 0 then \"negative\"\n else \"zero\"\n}\n\nfn main() {\n let msg = greet(\"demo user\")\n perform IO.print(msg)\n\n let f = factorial(6)\n perform IO.print(\"6! = \" + perform Int.to_string(f))\n\n let d = describe_number(42)\n perform IO.print(\"42 is \" + d)\n\n let s = add(100, 200)\n perform IO.print(\"100 + 200 = \" + perform Int.to_string(s))\n}\n",
),
],
"distributed" => vec![
(
"src/main.nula",
"// Distributed template — supervised, message-passing worker actors.\n//\n// Demonstrates: actor declaration, `spawn Actor {}`, message passing\n// with `!`, durable state per actor, and an OTP supervisor that\n// restarts a worker on abnormal exit.\n//\n// Run with: nula run\n\n// A worker actor. `count` is per-actor state mutated by the `work`\n// behavior; each spawned worker has its own independent copy.\nactor Worker {\n state count: Int = 0\n\n behavior work(by: Int) {\n self.count = self.count + by\n }\n\n behavior report() {\n perform IO.print(\" worker count=\" + perform Int.to_string(self.count))\n }\n}\n\nfn main() {\n // Spawn two independent workers and route work between them.\n let w1 = spawn Worker {}\n let w2 = spawn Worker {}\n\n w1 ! work(10)\n w1 ! work(5)\n w2 ! work(7)\n\n w1 ! report()\n w2 ! report()\n\n perform IO.print(\"Two distributed workers are running.\")\n}\n",
),
],
"ai-agent" => vec![
(
"src/main.nula",
"// AI-agent template — an actor with conversation memory backed by the\n// `Inference.ask` effect (LLM). Demonstrates actor state, behaviors,\n// and a non-blocking inference call.\n//\n// Requires the `ai-runtime` cargo feature (enabled by default).\n//\n// Run with: nula run\n\nactor ChatAgent {\n state history: String = \"\"\n state turn: Int = 0\n\n behavior ask(prompt: String) {\n self.turn = self.turn + 1\n let reply = perform Inference.ask(prompt)\n self.history = self.history + \"\\nQ: \" + prompt + \"\\nA: \" + reply\n perform IO.print(\"[Turn \" + perform Int.to_string(self.turn) + \"] \" + reply)\n }\n\n behavior summary() {\n let s = perform Inference.ask(\n \"Summarize this conversation:\\n\" + self.history\n )\n perform IO.print(\"Summary: \" + s)\n }\n}\n\nfn main() {\n let chat = spawn ChatAgent {}\n chat ! ask(\"Hello! Introduce yourself in one sentence.\")\n chat ! summary()\n perform IO.print(\"Agent ready. Configure your provider in Nulang.toml.\")\n}\n",
),
],
"web" => vec![
(
"src/main.nula",
"// Web template — HTTP client via the built-in `Http` effect.\n//\n// Demonstrates: `Http.get`, `Http.post`, and JSON payloads.\n//\n// Run with: nula run\n// Requires network access.\n\nfn main() {\n perform IO.print(\"HTTP client demo\")\n perform IO.print(\"---\")\n\n // GET a public endpoint and print how many bytes came back.\n let url = \"https://httpbin.org/get\"\n let resp = perform Http.get(url)\n perform IO.print(\"GET \" + url)\n perform IO.print(\" received \" + perform Int.to_string(perform String.length(resp)) + \" bytes\")\n\n // POST a JSON body and receive the echoed response.\n let body = \"{\\\"language\\\": \\\"Nulang\\\", \\\"features\\\": [\\\"actors\\\", \\\"effects\\\"]}\"\n let posted = perform Http.post(\"https://httpbin.org/post\", body)\n perform IO.print(\"POST JSON body\")\n perform IO.print(\" received \" + perform Int.to_string(perform String.length(posted)) + \" bytes\")\n\n perform IO.print(\"---\")\n perform IO.print(\"HTTP demo complete!\")\n}\n",
),
],
_ => unreachable!(),
}
}
/// Resolve the package in the current directory, write `Nulang.lock`, and
/// return the entry point path.
fn prepare_package() -> NuResult<PathBuf> {
let root = package_root()?;
let manifest_path = root.join(MANIFEST_FILE);
let manifest = Manifest::load(&root).map_err(|e| NuError::PackageError {
msg: format!(
"failed to load {} at {}: {}",
MANIFEST_FILE,
root.display(),
e
),
span: Span::default(),
})?;
if let Some(req) = &manifest.package.language {
use crate::format::constants::LANGUAGE_VERSION_STR;
let parse_maj_min = |s: &str| -> Option<(u32, u32)> {
let s = s.split('-').next().unwrap_or(s);
let mut parts = s.split('.');
let maj = parts.next()?.parse().ok()?;
let min = parts.next()?.parse().ok()?;
Some((maj, min))
};
if let (Some((req_maj, req_min)), Some((tool_maj, tool_min))) =
(parse_maj_min(req), parse_maj_min(LANGUAGE_VERSION_STR))
{
if req_maj != tool_maj || req_min != tool_min {
return Err(NuError::PackageError {
msg: format!(
"package requires language {} but this toolchain provides {}",
req, LANGUAGE_VERSION_STR
),
span: Span::default(),
});
}
} else {
// fallback exact match if parsing fails
let req_base = req.split('-').next().unwrap_or(req);
let tool_base = LANGUAGE_VERSION_STR
.split('-')
.next()
.unwrap_or(LANGUAGE_VERSION_STR);
if req_base != tool_base {
return Err(NuError::PackageError {
msg: format!(
"package requires language {} but this toolchain provides {}",
req, LANGUAGE_VERSION_STR
),
span: Span::default(),
});
}
}
}
eprintln!(" Resolving dependencies...");
let resolution = resolve(&root, &manifest).map_err(|e| NuError::PackageError {
msg: format!(
"failed to resolve dependencies for package '{}': {}\n help: check that all [dependencies] in {} are reachable",
manifest.package.name,
e,
manifest_path.display()
),
span: Span::default(),
})?;
let lock_path = root.join(LOCKFILE_FILE);
resolution
.to_lockfile()
.save(&root)
.map_err(|e| NuError::PackageError {
msg: format!("failed to write {}: {}", lock_path.display(), e),
span: Span::default(),
})?;
let entry = root.join(&manifest.package.entry);
if !entry.exists() {
return Err(NuError::PackageError {
msg: format!(
"entry point '{}' not found (defined as `entry = \"{}\"` in {})",
entry.display(),
manifest.package.entry,
manifest_path.display()
),
span: Span::default(),
});
}
Ok(entry)
}
/// Run the current `nulang` executable with `args`, inheriting stdio.
fn nulang_exe(args: &[&str]) -> NuResult<()> {
let exe = std::env::current_exe().map_err(|e| NuError::PackageError {
msg: format!("cannot locate nulang executable: {}", e),
span: Span::default(),
})?;
let mut cmd = Command::new(&exe);
cmd.args(args);
// Auto-detect the stdlib directory relative to the executable so
// that `import stdlib::*` works without setting NULANG_STDLIB.
if std::env::var_os("NULANG_STDLIB").is_none() {
if let Some(exe_dir) = exe.parent() {
let candidate = exe_dir.join("stdlib");
if candidate.is_dir() {
cmd.env("NULANG_STDLIB", &candidate);
}
}
}
let status = cmd.status().map_err(|e| NuError::PackageError {
msg: format!("failed to run nulang ({}): {}", exe.display(), e),
span: Span::default(),
})?;
if !status.success() {
return Err(NuError::PackageError {
msg: format!("nulang {} exited with {}", args.join(" "), status),
span: Span::default(),
});
}
Ok(())
}
/// `nula build`: resolve dependencies, write the lockfile, type-check entry.
/// `nula build`: resolve dependencies, write the lockfile, type-check and
/// compile to a .nbc artifact in .nula/dist/.
fn cmd_build() -> NuResult<()> {
let root = package_root()?;
let manifest_path = root.join(MANIFEST_FILE);
let manifest = Manifest::load(&root).map_err(|e| NuError::PackageError {
msg: format!("failed to load {}: {}", manifest_path.display(), e),
span: Span::default(),
})?;
let name = manifest.package.name.clone();
let entry = prepare_package()?;
let entry_str = entry.to_string_lossy().into_owned();
let dist_dir = root.join(".nula").join("dist");
std::fs::create_dir_all(&dist_dir).map_err(|e| NuError::PackageError {
msg: format!("cannot create {}: {}", dist_dir.display(), e),
span: Span::default(),
})?;
let nbc_path = dist_dir.join(format!("{}.nbc", name));
let nbc_path_str = nbc_path.to_string_lossy().into_owned();
eprintln!("Building {}...", name);
eprintln!(" Type-checking {}...", entry.display());
nulang_exe(&["--check", &entry_str])?;
eprintln!(" Compiling {} to .nbc...", name);
nulang_exe(&["--emit-nbc", "--out", &nbc_path_str, &entry_str])?;
println!("Build succeeded.");
Ok(())
}
/// `nula build-wasm`: compile package to .wasm + AOT .cwasm.
/// `nula build-wasm`: compile package to .wasm + AOT .cwasm in .nula/dist/.
fn cmd_build_wasm() -> NuResult<()> {
let root = package_root()?;
let manifest_path = root.join(MANIFEST_FILE);
let manifest = Manifest::load(&root).map_err(|e| NuError::PackageError {
msg: format!("failed to load {}: {}", manifest_path.display(), e),
span: Span::default(),
})?;
let name = manifest.package.name.clone();
let entry = prepare_package()?;
let entry_str = entry.to_string_lossy().into_owned();
let dist_dir = root.join(".nula").join("dist");
std::fs::create_dir_all(&dist_dir).map_err(|e| NuError::PackageError {
msg: format!("cannot create {}: {}", dist_dir.display(), e),
span: Span::default(),
})?;
let wasm_path = dist_dir.join(format!("{}.wasm", name));
let wasm_path_str = wasm_path.to_string_lossy().into_owned();
eprintln!("Building {} (WASM AOT)...", name);
eprintln!(" Compiling {} to WASM...", entry.display());
nulang_exe(&["--backend", "wasm-aot", "--out", &wasm_path_str, &entry_str])?;
println!("WASM AOT build succeeded.");
Ok(())
}
/// `nula run`: build, then execute the entry point.
fn cmd_run() -> NuResult<()> {
eprintln!("Building and running...");
let entry = prepare_package()?;
let entry_str = entry.to_string_lossy().into_owned();
nulang_exe(&[&entry_str])
}
/// `nula run --watch` (or `nula watch`): build, run, and re-run when source
/// files change under `src/`. Uses simple mtime polling.
fn cmd_run_watch() -> NuResult<()> {
let root = package_root()?;
let entry = prepare_package()?;
let entry_str = entry.to_string_lossy().into_owned();
// Initial run
eprintln!("Building and running...");
nulang_exe(&[&entry_str])?;
// Collect initial mtimes for all .nula files under src/
let src_dir = root.join("src");
let mut last_mtimes = collect_mtimes(&src_dir);
println!("watching... (Ctrl-C to stop)");
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
let current = collect_mtimes(&src_dir);
if current != last_mtimes {
last_mtimes = current;
eprintln!("\n--- change detected, rebuilding ---");
// Re-resolve in case dependencies changed
match prepare_package() {
Ok(entry) => {
let es = entry.to_string_lossy().into_owned();
let _ = nulang_exe(&[&es]);
}
Err(e) => eprintln!("Error: {}", e),
}
}
}
}
/// Collect (path, mtime) pairs for all .nula files under `dir`, sorted by path.
fn collect_mtimes(dir: &Path) -> Vec<(PathBuf, std::time::SystemTime)> {
let mut result = Vec::new();
collect_mtimes_recursive(dir, &mut result);
result.sort_by(|a, b| a.0.cmp(&b.0));
result
}
fn collect_mtimes_recursive(dir: &Path, out: &mut Vec<(PathBuf, std::time::SystemTime)>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_mtimes_recursive(&path, out);
} else if path.extension().is_some_and(|ext| ext == "nula") {
if let Ok(meta) = std::fs::metadata(&path) {
if let Ok(mtime) = meta.modified() {
out.push((path, mtime));
}
}
}
}
}
/// `nula test [--filter <substr>] [--verbose|-v]`: discover and run `.nula`
/// test files under the package's `tests/` directory, reporting pass/fail.
///
/// Each test file is executed via the `nulang` exe in the current package
/// (same process as `nula run`). A test PASSes if it runs to completion
/// without error; any compile or runtime error (including assertion
/// failures from the `Test` effect) is a FAIL.
///
/// With `--verbose` (or `-v`): prints each test file name before execution,
/// shows ✓ PASS / ✗ FAIL per file, and displays error messages for failures.
/// Default (non-verbose) output is clean and greppable.
fn cmd_test(filter: Option<&str>, verbose: bool) -> NuResult<()> {
eprintln!("Preparing package...");
let _entry = prepare_package()?;
let tests_dir = package_root()?.join("tests");
let mut test_files: Vec<PathBuf> = match std::fs::read_dir(&tests_dir) {
Ok(entries) => entries
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|ext| ext == "nula"))
.filter(|p| {
filter.map_or(true, |f| {
p.file_stem()
.and_then(|s| s.to_str())
.map_or(false, |s| s.contains(f))
})
})
.collect(),
Err(_) => Vec::new(),
};
test_files.sort();
if test_files.is_empty() {
println!("No tests found in {}", tests_dir.display());
return Ok(());
}
// Phase 1: discover per-function tests (fn test_*)
struct TestCase {
display: String,
file_to_run: PathBuf,
is_temp: bool,
}
let mut tests: Vec<TestCase> = Vec::new();
let temp_dir = std::env::temp_dir().join("nula_test");
let _ = std::fs::create_dir_all(&temp_dir);
for file in &test_files {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => continue,
};
let test_fns = discover_test_functions(&content);
let relative = file
.strip_prefix(&tests_dir.parent().unwrap_or(&tests_dir))
.unwrap_or(file);
if test_fns.is_empty() {
// No test_* functions: run whole file as one test
tests.push(TestCase {
display: relative.display().to_string(),
file_to_run: file.clone(),
is_temp: false,
});
} else {
if verbose {
println!("--- {} ---", relative.display());
println!(" discovered: {}", test_fns.join(", "));
}
// Strip fn main() for per-function wrappers
let stripped = strip_main_function(&content);
for fn_name in &test_fns {
let wrapper = format!(
"{}{}\nfn main() {{ {}() }}\n",
stripped,
if stripped.ends_with('\n') { "" } else { "\n" },
fn_name
);
let temp_path = temp_dir.join(format!("test_{}.nula", fn_name));
if std::fs::write(&temp_path, &wrapper).is_err() {
continue;
}
tests.push(TestCase {
display: fn_name.clone(),
file_to_run: temp_path,
is_temp: true,
});
}
}
}
eprintln!("running {} tests", tests.len());
let mut passed = 0;
let mut failed = 0;
for test in &tests {
let file_str = test.file_to_run.to_string_lossy().into_owned();
match run_test_file(&file_str) {
Ok(()) => {
passed += 1;
println!("test {} ... ok", test.display);
}
Err(stderr_output) => {
failed += 1;
if verbose {
println!("test {} ... FAILED", test.display);
for line in stderr_output.lines() {
println!(" {}", line);
}
} else {
println!("test {} ... FAILED", test.display);
eprintln!("{}", stderr_output.trim());
}
}
}
}
// Clean up temp files
for test in &tests {
if test.is_temp {
let _ = std::fs::remove_file(&test.file_to_run);
}
}
let _ = std::fs::remove_dir(&temp_dir);
println!("\ntest result: {} passed; {} failed", passed, failed);
if failed > 0 {
return Err(NuError::PackageError {
msg: format!("{} test(s) failed", failed),
span: Span::default(),
});
}
Ok(())
}
/// `nula test --watch` (or `nula test -w`): run tests and re-run when source
/// files change under `src/` or `tests/`. Uses simple mtime polling.
fn cmd_test_watch(filter: Option<&str>, verbose: bool) -> NuResult<()> {
let root = package_root()?;
// Initial run
let _ = cmd_test(filter, verbose);
// Collect initial mtimes for all .nula files under src/ and tests/
let src_dir = root.join("src");
let tests_dir = root.join("tests");
let mut last_src = collect_mtimes(&src_dir);
let mut last_tests = collect_mtimes(&tests_dir);
println!("watching for changes... (Ctrl-C to stop)");
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
let current_src = collect_mtimes(&src_dir);
let current_tests = collect_mtimes(&tests_dir);
if current_src != last_src || current_tests != last_tests {
last_src = current_src;
last_tests = current_tests;
// Clear screen
print!("\x1B[2J\x1B[H");
eprintln!("re-running tests...");
let _ = cmd_test(filter, verbose);
}
}
}
/// Find all `fn test_*` function names in a source string.
fn discover_test_functions(source: &str) -> Vec<String> {
let mut names = Vec::new();
let mut rest = source;
while let Some(pos) = rest.find("fn test_") {
let start = pos + 3; // skip "fn "
let after_fn = &rest[start..];
// Find end of identifier: whitespace or '('
let end = after_fn
.find(|c: char| c.is_whitespace() || c == '(')
.unwrap_or(after_fn.len());
let name = after_fn[..end].trim().to_string();
if !name.is_empty() {
names.push(name);
}
rest = &after_fn[end..];
}
names
}
/// Strip the `fn main() { ... }` block from source, keeping everything else.
fn strip_main_function(source: &str) -> String {
if let Some(pos) = source.find("fn main") {
// Find the opening brace after "fn main"
if let Some(brace_start) = source[pos..].find('{') {
let abs_brace = pos + brace_start;
// Count braces to find matching close
let mut depth = 0;
let mut end = abs_brace;
for (i, ch) in source[abs_brace..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = abs_brace + i + 1;
break;
}
}
_ => {}
}
}
let before = &source[..pos];
let after = &source[end..];
return format!("{}{}", before, after);
}
}
source.to_string()
}
/// Run a test file via `nulang`, capturing stderr so error messages appear
/// after the test name (avoiding interleaved output).
/// Returns `Ok(())` on success, `Err(stderr_string)` on failure.
fn run_test_file(file_path: &str) -> Result<(), String> {
let exe = std::env::current_exe().map_err(|e| format!("cannot locate nulang: {}", e))?;
let mut cmd = Command::new(&exe);
cmd.arg(file_path);
cmd.stdout(std::process::Stdio::inherit());
cmd.stderr(std::process::Stdio::piped());
if std::env::var_os("NULANG_STDLIB").is_none() {
if let Some(exe_dir) = exe.parent() {
let candidate = exe_dir.join("stdlib");
if candidate.is_dir() {
cmd.env("NULANG_STDLIB", &candidate);
}
}
}
let output = cmd
.output()
.map_err(|e| format!("failed to run nulang: {}", e))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Err(stderr)
}
}
/// `nula list`: print all locked dependencies with versions and sources.
fn cmd_list() -> NuResult<()> {
let root = package_root()?;
let lock_path = root.join(LOCKFILE_FILE);
let lockfile = Lockfile::load(&root).map_err(|e| NuError::PackageError {
msg: format!(
"failed to read {}: {}\n hint: run 'nulang nula build' first to generate it",
lock_path.display(),
e
),
span: Span::default(),
})?;
if lockfile.package.is_empty() {
println!("No dependencies locked.");
return Ok(());
}
println!("Locked dependencies (from {}):", lock_path.display());
for pkg in &lockfile.package {
println!(" {} v{} — {}", pkg.name, pkg.version, pkg.source);
}
Ok(())
}
/// `nula clean`: remove build artifacts (.nbc files).
/// `nula clean`: remove build artifacts (.nula/dist/ directory).
fn cmd_clean() -> NuResult<()> {
let root = package_root()?;
let dist_dir = root.join(".nula").join("dist");
if dist_dir.exists() {
eprintln!("Cleaning build artifacts...");
std::fs::remove_dir_all(&dist_dir).map_err(|e| NuError::PackageError {
msg: format!("cannot remove {}: {}", dist_dir.display(), e),
span: Span::default(),
})?;
println!("Removed build artifacts.");
} else {
println!("No build artifacts found.");
}
Ok(())
}
/// `nula doc [--open]`: generate Markdown API docs for the package.
///
/// Scans all `.nula` files under `src/`, extracts doc comments (`///` and
/// `//!`) and declarations (`fn`, `actor`, `type`, `workflow`), and writes
/// a combined `docs/api.md`. With `--open`, spawns `xdg-open` on the
/// output file (best-effort).
fn cmd_doc(open: bool) -> NuResult<()> {
let root = package_root()?;
let manifest_path = root.join(MANIFEST_FILE);
if !manifest_path.exists() {
return Err(NuError::PackageError {
msg: format!(
"no {} found in {} — run 'nulang nula init' first",
MANIFEST_FILE,
root.display()
),
span: Span::default(),
});
}
let out_path = crate::docgen::write_package_docs(&root)?;
println!("Wrote {}", out_path.display());
if open {