nomen-lang 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ // Fiber — a stackful coroutine (ASYNC.md Phase 1). Fibers multiplex
2
+ // over the worker pool's threads: a parked fiber frees its worker instead of
3
+ // blocking it, so thousands of waiting tasks cost stacks (~64 KB each), not
4
+ // threads (~MB each).
5
+ //
6
+ // `Fiber(fn(args)).start()` mirrors `Thread(fn(args)).start()` — same eager
7
+ // arg packing into a task closure, same future, same Task<T> handle — but
8
+ // the wrapped call runs on a fiber stack under the fiber scheduler. While
9
+ // it waits (Task.result/wait, Channel.receive), the fiber parks and the
10
+ // worker moves on; `Fiber.yield()` cooperatively hands the worker to the
11
+ // next runnable fiber.
12
+ //
13
+ // Like Thread, a Fiber construction is a real value: it can be stored and
14
+ // started later (`var f = Fiber(work(n))`, then `f.start()` or
15
+ // `f.start_on(buf)`), and destroying an unstarted Fiber is a programming
16
+ // error that #destroy reports and aborts on (must-start — the library
17
+ // pattern from ASYNC.md).
18
+ //
19
+ // `Fiber(fn(args)).start_on(buf)` runs the fiber on a caller-provided
20
+ // fixed-size array stack (>= 16 KB) — the embedded/no-heap path.
21
+ //
22
+ // `Fiber.set_cooperative(true)` (before the first spawn) runs fibers on the
23
+ // calling thread and never starts worker threads — a cooperative,
24
+ // single-threaded runtime for bare-metal targets.
25
+ //
26
+ // The constructor and `.start_on(buf)` are compiler-special-cased (they
27
+ // need the per-site spawn/trampoline machinery and the buffer's compile-time
28
+ // byte size); `.start()` is a regular method over the runtime seam — it
29
+ // reads the packed handles from the instance's fields and drives the
30
+ // scheduler. The runtime (scheduler, context switch) lives in the build's
31
+ // emitted companion C — see build_fiber_spawn.
32
+
33
+ // The launch surface's return type (ASYNC.md: the dependency is
34
+ // declared by the library file, not inferred from user-source tokens).
35
+ import Task
36
+
37
+ /**
38
+ * A stackful coroutine: lightweight, cooperatively scheduled, parks instead of blocking
39
+ **/
40
+ pub class Fiber<T> : Sendable, Spawnable<T> {
41
+ // The construction hook (see Thread.nm): the deferred-call special form.
42
+ #spawn = () {
43
+ }
44
+
45
+ // The packed task closure (a `struct nomen_closure *`); owned by this
46
+ // value until start/start_on/nursery.start transfers it to the runtime.
47
+ pub var uint64 task = 0
48
+ // Result-slot / cancel-flag / future handles for the (single) launch.
49
+ pub var uint64 result_slot = 0
50
+ pub var uint64 cancel_flag = 0
51
+ pub var uint64 future = 0
52
+ // Must-start flag: set by .start() / .start_on(buf) / nursery.start.
53
+ pub var started = false
54
+ // LEXICAL NURSERY CAPTURE (ASYNC.md): the tracking slots of the
55
+ // `async` block that lexically encloses the CONSTRUCTION, or 0 when the
56
+ // construction is outside any block. `.start()` registers the future
57
+ // through these stored pointers — registration follows the scope that
58
+ // created the deferred call, not the one that happens to start it. See
59
+ // Thread.nm's matching fields.
60
+ pub var uint64 nursery_futures = 0
61
+ pub var uint64 nursery_count = 0
62
+ pub var uint64 nursery_cap = 0
63
+
64
+ // Start the deferred call on the fiber scheduler and yield the Task<T>
65
+ // handle. Same shape as Thread.start: consume the machinery, register
66
+ // with the construction-time nursery (if captured), transfer the handles
67
+ // out of the instance (started; #destroy no-ops), return the handle.
68
+ // While the fiber waits, it parks and frees its worker instead of
69
+ // blocking it.
70
+ pub func start = (ref self, move out Task<T>) {
71
+ ```
72
+ #arch: c
73
+ struct nomen_future *_f = (struct nomen_future *)self->future;
74
+ _f->refs = self->nursery_futures ? 3 : 2;
75
+ __nomen_fiber_spawn((struct nomen_closure *)self->task, _f);
76
+ if (self->nursery_futures) {
77
+ __nomen_nursery_track((void **)self->nursery_futures, (int *)self->nursery_count, (int *)self->nursery_cap, _f);
78
+ }
79
+ ```
80
+ ```
81
+ #arch: aarch64
82
+ // x19 = self. f = self->future (#32); nursery_futures at #48.
83
+ ldr x0, [x19, #32]
84
+ ldr x1, [x19, #48]
85
+ mov w2, #2
86
+ cbz x1, .Lnomen_fstart_refs
87
+ mov w2, #3
88
+ .Lnomen_fstart_refs:
89
+ str w2, [x0, #116]
90
+ // __nomen_fiber_spawn(self->task, f)
91
+ ldr x0, [x19, #8]
92
+ ldr x1, [x19, #32]
93
+ bl ___nomen_fiber_spawn
94
+ // if (self->nursery_futures) __nomen_nursery_track(futures, count, cap, f)
95
+ ldr x0, [x19, #48]
96
+ cbz x0, .Lnomen_fstart_notrack
97
+ ldr x1, [x19, #56]
98
+ ldr x2, [x19, #64]
99
+ ldr x3, [x19, #32]
100
+ bl ___nomen_nursery_track
101
+ .Lnomen_fstart_notrack:
102
+ ```
103
+ var t = Task<T>()
104
+ t.result_slot = self.result_slot
105
+ t.cancel_flag = self.cancel_flag
106
+ t.future = self.future
107
+ // Transfer the handles out of the instance: the runtime owns
108
+ // everything now, and #destroy must not touch (or must-start-abort).
109
+ self.task = 0
110
+ self.future = 0
111
+ self.result_slot = 0
112
+ self.cancel_flag = 0
113
+ self.started = true
114
+ return t
115
+ }
116
+
117
+ // Run the deferred call fire-and-forget (the Spawnable daemon role).
118
+ // The fiber is launched exactly like start, but the handle is dropped
119
+ // immediately: the future's refs are released as the discarded Task is
120
+ // reclaimed, and the fiber completes into the void — nothing joins it.
121
+ pub func detach = (ref self) {
122
+ self.start()
123
+ }
124
+
125
+ // Destroying an unstarted Fiber is a programming error (the wrapped
126
+ // call would silently never run) — report it and abort. A started
127
+ // Fiber owns nothing: the launch transferred the task closure, result
128
+ // slot, cancel flag, and future to the runtime, and cleared the fields.
129
+ pub func #destroy = () {
130
+ ```
131
+ #arch: c
132
+ if (!self->started) {
133
+ __nomen_spawn_must_start_abort();
134
+ }
135
+ if (self->future) {
136
+ __nomen_future_release((struct nomen_future *)self->future);
137
+ self->future = 0;
138
+ self->result_slot = 0;
139
+ self->cancel_flag = 0;
140
+ }
141
+ ```
142
+ ```
143
+ #arch: aarch64
144
+ // if (!self->started) __nomen_spawn_must_start_abort()
145
+ ldrb w0, [x19, #40]
146
+ cbnz w0, .Lfb_started
147
+ bl ___nomen_spawn_must_start_abort
148
+ .Lfb_started:
149
+ // if (self->future) { release it (an unstarted task's future is
150
+ // unshared — refs == 1 — so this tears down slot + flag + future);
151
+ // zero the fields }
152
+ ldr x0, [x19, #32]
153
+ cbz x0, .Lfb_done
154
+ bl ___nomen_future_release
155
+ str xzr, [x19, #32]
156
+ str xzr, [x19, #16]
157
+ str xzr, [x19, #24]
158
+ .Lfb_done:
159
+ ```
160
+ }
161
+
162
+ // Hand the worker to the next runnable fiber. No-op outside a fiber.
163
+ pub func yield = () {
164
+ ```
165
+ #arch: c
166
+ __nomen_fiber_yield();
167
+ ```
168
+ ```
169
+ #arch: aarch64
170
+ bl ___nomen_fiber_yield
171
+ ```
172
+ }
173
+
174
+ // Is the calling code running inside a fiber? True when Fiber.yield or
175
+ // fiber parking would take effect.
176
+ pub func is_fiber = (out bool) {
177
+ ```
178
+ #arch: c
179
+ return __nomen_fiber_is_active();
180
+ ```
181
+ ```
182
+ #arch: aarch64
183
+ bl ___nomen_fiber_is_active
184
+ ```
185
+ }
186
+
187
+ // Run fibers on the calling thread and never start worker threads. Must
188
+ // be called before the first fiber spawn; intended for single-threaded
189
+ // (bare-metal) targets. In cooperative mode a fiber that parks on an
190
+ // event only another thread could fire will never resume.
191
+ pub func set_cooperative = (bool on) {
192
+ ```
193
+ #arch: c
194
+ __nomen_fiber_set_cooperative(on ? 1 : 0);
195
+ ```
196
+ ```
197
+ #arch: aarch64
198
+ bl ___nomen_fiber_set_cooperative
199
+ ```
200
+ }
201
+ }
@@ -7,7 +7,7 @@
7
7
  * A graph of values connected by edges
8
8
  **/
9
9
  pub struct Graph<T>: Enumerable {
10
- var Buffer<T> values = Buffer<T>()
10
+ var values = Buffer<T>()
11
11
  var head_edge = Buffer<int>()
12
12
  var edge_target = Buffer<int>()
13
13
  var edge_next = Buffer<int>()
@@ -7,7 +7,7 @@
7
7
  * A doubly-linked list of values
8
8
  **/
9
9
  pub struct LinkedList<T>: Enumerable {
10
- var Buffer<T> values = Buffer<T>()
10
+ var values = Buffer<T>()
11
11
  var nexts = Buffer<int>()
12
12
  var prevs = Buffer<int>()
13
13
  var int head = -1
@@ -3,7 +3,7 @@
3
3
  **/
4
4
  pub struct List<T>: Viewable {
5
5
  var length = 0
6
- var Buffer<T> items = Buffer<T>()
6
+ var items = Buffer<T>()
7
7
 
8
8
  pub func push = (ref self, move T value) {
9
9
  if self.length >= self.items.cap {
@@ -70,8 +70,8 @@ pub struct List<T>: Viewable {
70
70
  // never re-checked, so it can't rely on check-time call-arg hoisting for
71
71
  // struct element types.
72
72
  pub func copy = (self, out List<T>) {
73
- var List<T> dst = List<T>()
74
- var int i = 0
73
+ var dst = List<T>()
74
+ var i = 0
75
75
  while i < self.length {
76
76
  var T v = self.at(i)
77
77
  dst.push(v)
@@ -10,8 +10,8 @@
10
10
  **/
11
11
  pub struct Map<TK: Hashable + Equatable, TV> {
12
12
  var length = 0
13
- var Buffer<TK> keys = Buffer<TK>()
14
- var Buffer<TV> values = Buffer<TV>()
13
+ var keys = Buffer<TK>()
14
+ var values = Buffer<TV>()
15
15
  var used = Buffer<int>()
16
16
 
17
17
  // Variadic-tuple constructor: Map<string, int>(["a", 1], ["b", 2]) or
@@ -3,6 +3,12 @@
3
3
  // Default stance in Nomen is "no shared mutable state" — communicate by moving
4
4
  // Sendable values (or via Channel<T>). Mutex exists for when you genuinely
5
5
  // need shared mutability. See ASYNC.md.
6
+ //
7
+ // A fiber that would block on a contended lock parks instead of blocking its
8
+ // worker (both models), and unlock wakes the parked fibers. Cancellation
9
+ // while waiting never returns without the lock — a waiter keeps waiting and
10
+ // observes the cancel flag at its next checkpoint after acquiring — so a
11
+ // matching unlock is always sound.
6
12
 
7
13
  /**
8
14
  * A pthread-backed mutual-exclusion lock for protecting shared mutable state
@@ -11,68 +17,58 @@ pub class Mutex: Sendable {
11
17
  pub var uint64 handle = 0
12
18
 
13
19
  pub func #init = (self) {
14
- ```
15
- #arch: c
16
- #scope: file
17
- #include <pthread.h>
18
- ```
19
- ```
20
- #arch: c
21
- self->handle = (unsigned long long)malloc(sizeof(pthread_mutex_t));
22
- pthread_mutex_init((pthread_mutex_t *)self->handle, NULL);
23
- ```
24
- ```
25
- #arch: aarch64
26
- mov x0, #64
27
- bl _malloc
28
- str x0, [x19, #8]
29
- mov x1, #0
30
- bl _pthread_mutex_init
31
- ```
20
+ ```
21
+ #arch: c
22
+ self->handle = (unsigned long long)__nomen_mutex_create();
23
+ ```
24
+ ```
25
+ #arch: aarch64
26
+ bl ___nomen_mutex_create
27
+ str x0, [x19, #8]
28
+ ```
32
29
  }
33
30
 
34
31
  pub func lock = (self) {
35
- ```
36
- #arch: c
37
- pthread_mutex_lock((pthread_mutex_t *)self->handle);
38
- ```
39
- ```
40
- #arch: aarch64
41
- ldr x0, [x19, #8]
42
- bl _pthread_mutex_lock
43
- ```
32
+ ```
33
+ #arch: c
34
+ // Fibers park instead of blocking the worker (both models); other
35
+ // contexts take the pthread lock directly.
36
+ __nomen_mutex_lock((void *)self->handle);
37
+ ```
38
+ ```
39
+ #arch: aarch64
40
+ ldr x0, [x19, #8]
41
+ bl ___nomen_mutex_lock
42
+ ```
44
43
  }
45
44
 
46
45
  pub func unlock = (self) {
47
- ```
48
- #arch: c
49
- pthread_mutex_unlock((pthread_mutex_t *)self->handle);
50
- ```
51
- ```
52
- #arch: aarch64
53
- ldr x0, [x19, #8]
54
- bl _pthread_mutex_unlock
55
- ```
46
+ ```
47
+ #arch: c
48
+ __nomen_mutex_unlock_wake((void *)self->handle);
49
+ ```
50
+ ```
51
+ #arch: aarch64
52
+ ldr x0, [x19, #8]
53
+ bl ___nomen_mutex_unlock_wake
54
+ ```
56
55
  }
57
56
 
58
57
  pub func #destroy = () {
59
- ```
60
- #arch: c
61
- if (self->handle) {
62
- pthread_mutex_destroy((pthread_mutex_t *)self->handle);
63
- free((void *)self->handle);
64
- self->handle = 0;
65
- }
66
- ```
67
- ```
68
- #arch: aarch64
69
- ldr x0, [x19, #8]
70
- cbz x0, .Lmutex_destroy_end
71
- bl _pthread_mutex_destroy
72
- ldr x0, [x19, #8]
73
- bl _free
74
- str xzr, [x19, #8]
75
- .Lmutex_destroy_end:
76
- ```
58
+ ```
59
+ #arch: c
60
+ if (self->handle) {
61
+ __nomen_mutex_dispose((void *)self->handle);
62
+ self->handle = 0;
63
+ }
64
+ ```
65
+ ```
66
+ #arch: aarch64
67
+ ldr x0, [x19, #8]
68
+ cbz x0, .Lmutex_destroy_end
69
+ bl ___nomen_mutex_dispose
70
+ str xzr, [x19, #8]
71
+ .Lmutex_destroy_end:
72
+ ```
77
73
  }
78
74
  }
@@ -5,18 +5,19 @@
5
5
  // An `async` block names its nursery (`async pool { }`), binding a Nursery
6
6
  // variable in scope. That variable is passed to helper functions with `ref`
7
7
  // and is the receiver of `pool.spawn(fn(args))`. The Nursery wraps the
8
- // per-invocation tracking state of the `async { }` block — a pointer to the
9
- // futures array and a pointer to the count slot, both on the async block's
10
- // stack frame — so it is only valid for the lifetime of its enclosing block
11
- // (which is exactly the structured-concurrency invariant: the block cannot
12
- // exit until every spawned task, including those spawned through a passed
13
- // Nursery, has finished).
8
+ // per-invocation tracking state of the `async { }` block — pointers to the
9
+ // futures list's storage slot, the registered count, and the capacity (the
10
+ // list starts empty and grows by realloc; the compiler's registration helper
11
+ // writes through these pointers) — so it is only valid for the lifetime of
12
+ // its enclosing block (which is exactly the structured-concurrency
13
+ // invariant: the block cannot exit until every spawned task, including those
14
+ // spawned through a passed Nursery, has finished).
14
15
  //
15
16
  // Config rides on the declaration: `async pool = Nursery(timeout: 2000) { }`.
16
17
  //
17
18
  // `name.spawn(fn(args))` is special-cased by the compiler (it needs the spawn
18
- // trampoline machinery), so it is NOT a regular method declared here. The two
19
- // fields are the full runtime contract.
19
+ // trampoline machinery), so it is NOT a regular method declared here. The
20
+ // three fields are the full runtime contract.
20
21
 
21
22
  /**
22
23
  * The capability for spawning into a caller's `async` block; owns and tracks its tasks
@@ -24,4 +25,5 @@
24
25
  pub struct Nursery: Sendable {
25
26
  pub var uint64 futures_ptr = 0
26
27
  pub var uint64 count_ptr = 0
28
+ pub var uint64 cap_ptr = 0
27
29
  }
@@ -1,4 +1,4 @@
1
1
  pub enum Option<T> {
2
- case some(T value)
3
- case none
2
+ case some(T value)
3
+ case none
4
4
  }
@@ -0,0 +1,67 @@
1
+ // Random — a small, fast pseudo-random generator (splitmix64).
2
+ //
3
+ // Splitmix64 steps a 64-bit state by the golden ratio, then runs two
4
+ // multiply-xor rounds. Streams from nearby seeds are fully decorrelated
5
+ // (each seed is its own stream), so `Random()` — seeded from the wall
6
+ // clock — is safe to construct repeatedly in quick succession (e.g. one
7
+ // per spawned task, all starting within the same microsecond).
8
+ //
9
+ // Not cryptographically secure; `below`/`range` have slight modulo bias
10
+ // for very large bounds.
11
+ //
12
+ // var Random rng = Random()
13
+ // var uint64 roll = rng.below(6) // 0..5
14
+ // var uint64 ms = rng.range(1000, 10000) // 1000..10000, inclusive
15
+ //
16
+ import Time
17
+
18
+ /**
19
+ * A fast splitmix64 pseudo-random number generator
20
+ **/
21
+ pub class Random {
22
+ // The raw 64-bit generator state (seeded from the wall clock by the
23
+ // no-arg constructor, or an explicit seed for reproducible sequences).
24
+ pub var uint64 state = 0
25
+
26
+ // Seed from the wall clock (nanoseconds since the epoch).
27
+ pub func #init = (self) {
28
+ self.state = Time.now_ns()
29
+ }
30
+
31
+ // Seed from an explicit value — reproducible sequences for tests.
32
+ pub func #init = (self, uint64 seed) {
33
+ self.state = seed
34
+ }
35
+
36
+ // The next 64-bit value (splitmix64: golden-ratio step, then two
37
+ // multiply-xor rounds). Pure Nomen — the 64-bit arithmetic, bitwise
38
+ // operators, and hex literals all lower directly. The aarch64 codegen is
39
+ // a little looser than a hand-written `#arch` body would be (constant
40
+ // materialization, field reloads, non-immediate shift counts); see
41
+ // FOLLOWUP.md if this ever needs to be hot.
42
+ pub func next = (ref self, out uint64) {
43
+ self.state = self.state + 0x9E3779B97F4A7C15
44
+ var uint64 z = self.state
45
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
46
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EB
47
+ return z ^ (z >> 31)
48
+ }
49
+
50
+ // The next value in [0, bound). A bound of 0 returns 0. Slight modulo
51
+ // bias for very large bounds — irrelevant at demo scale.
52
+ pub func below = (ref self, uint64 bound, out uint64) {
53
+ if bound == 0 {
54
+ return 0
55
+ }
56
+ return self.next() % bound
57
+ }
58
+
59
+ // The next value in [lo, hi], both ends inclusive. A hi <= lo
60
+ // returns lo.
61
+ pub func range = (ref self, uint64 lo, uint64 hi, out uint64) {
62
+ if hi <= lo {
63
+ return lo
64
+ }
65
+ return lo + self.below(hi - lo + 1)
66
+ }
67
+ }
@@ -1,4 +1,4 @@
1
1
  pub must_use enum Result<T, E> {
2
- case ok(T value)
3
- case error(E error)
2
+ case ok(T value)
3
+ case error(E error)
4
4
  }
@@ -9,7 +9,7 @@
9
9
  **/
10
10
  pub struct Set<T: Hashable + Equatable> {
11
11
  var length = 0
12
- var Buffer<T> slots = Buffer<T>()
12
+ var slots = Buffer<T>()
13
13
  var used = Buffer<int>()
14
14
 
15
15
  pub func add = (ref self, move T value) {
@@ -0,0 +1,18 @@
1
+ // Spawnable — the trait role of a deferred-work LAUNCHER (the C# shape;
2
+ // see docs/ASYNC.md, "Design decisions"). A Spawnable class packs a deferred unit of work
3
+ // (the construction special form) and its `start` submits it, yielding the
4
+ // library-owned `Task<T>` handle — so the await half is guaranteed by the
5
+ // return type and each type carries exactly one role: `Thread<T>`/`Fiber<T>`
6
+ // are spawnable, `Task<T>` is awaitable. `Awaitable` stays non-generic (its
7
+ // `wait` does not mention T); `Spawnable` is generic because `start`'s
8
+ // return type does.
9
+ //
10
+ // `detach` is the daemon role: run the work unjoinable, statement only.
11
+
12
+ /**
13
+ * A class that can launch a deferred call and hand back its Task<T> handle
14
+ **/
15
+ pub trait Spawnable<T> {
16
+ func start = (ref self, move out Task<T>)
17
+ func detach = (ref self)
18
+ }
@@ -39,7 +39,7 @@ pub struct File {
39
39
  pub var eof = false
40
40
  // errno of the last operation; 0 = success. Raw #arch bodies set it,
41
41
  // the Result-returning wrappers map it to FileError.
42
- var int error = 0
42
+ var error = 0
43
43
 
44
44
  // Maps an errno to its FileError case.
45
45
  func error_of = (int err, out FileError) {
@@ -569,7 +569,7 @@ pub struct File {
569
569
  // (static) Read the entire contents of `path` in one open/read/close.
570
570
  pub func read_all = (string path, out Result<string, FileError>) {
571
571
  var Result<string, FileError> result = .error(FileError.other)
572
- var File f = File()
572
+ var f = File()
573
573
  match f.open(path, "r") {
574
574
  case .ok(did) {
575
575
  match f.readAll() {
@@ -594,7 +594,7 @@ pub struct File {
594
594
  // be deliberate).
595
595
  pub func write_all = (string path, string data, out Result<bool, FileError>) {
596
596
  var Result<bool, FileError> result = .error(FileError.other)
597
- var File f = File()
597
+ var f = File()
598
598
  match f.open(path, "w") {
599
599
  case .ok(did) {
600
600
  match f.writeAll(data) {