Add default parameter values, fix relative imports, and add WaitQueue deque ops

Parser:
- Fix parseGenericConstraint to recognize '=' as a stop token, enabling
  fn foo(x: int64 = 0) syntax
- Fix parseModulePath and parseImportPath to handle '../' as a single
  token (the lexer greedily merges '..' and '/')

Typechecker:
- Relax isCapable to accept fewer arguments when trailing parameters
  have defaults
- Fill in default values for missing arguments in lowerResolvedCall

C runtime:
- Add peon_wait_queue_pop_back and peon_wait_queue_wake_last for O(1)
  tail removal, making WaitQueue a proper deque
- Rename wake_one -> wake_first for symmetry with wake_last

Stdlib:
- Move sync.pn into sync/ directory (events.pn, queues.pn)
- Fix Event.set() missing self.signaled = true assignment (deadlock bug)
- Add wakeLast and isEmpty bindings for WaitQueue
- Fix WaitQueue clone to use UFCS init() style

Syntax highlighting:
- Fix import path regex to support multiple ../ prefixes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 02:44:24 +02:00
parent f8cf38b443
commit 889a14731c
11 changed files with 217 additions and 61 deletions

View File

@@ -222,7 +222,7 @@
"imports": {
"patterns": [
{
"match": "\\b(from)\\b\\s+((?:\\.\\./|[A-Za-z_][A-Za-z0-9_]*)(?:/[A-Za-z_][A-Za-z0-9_]*)*)\\s+(import)\\b",
"match": "\\b(from)\\b\\s+((?:\\.\\./)*[A-Za-z_][A-Za-z0-9_]*(?:/[A-Za-z_][A-Za-z0-9_]*)*)\\s+(import)\\b",
"captures": {
"1": {
"name": "keyword.control.import.peon"
@@ -236,7 +236,7 @@
}
},
{
"match": "\\b(import)\\b\\s+((?:\\.\\./|[A-Za-z_][A-Za-z0-9_]*)(?:/[A-Za-z_][A-Za-z0-9_]*)*)",
"match": "\\b(import)\\b\\s+((?:\\.\\./)*[A-Za-z_][A-Za-z0-9_]*(?:/[A-Za-z_][A-Za-z0-9_]*)*)",
"captures": {
"1": {
"name": "keyword.control.import.peon"

View File

@@ -356,8 +356,10 @@ PeonSpawnHandle peon_spawn(PeonNursery *nursery,
bool peon_spawn_handle_is_complete(PeonSpawnHandle handle);
void peon_spawn_handle_take_result(PeonSpawnHandle handle, void *out_value);
void peon_wait_queue_init(PeonWaitQueue *queue);
bool peon_wait_queue_wake_one(PeonWaitQueue *queue);
bool peon_wait_queue_wake_first(PeonWaitQueue *queue);
bool peon_wait_queue_wake_last(PeonWaitQueue *queue);
int64_t peon_wait_queue_wake_all(PeonWaitQueue *queue);
bool peon_wait_queue_is_empty(PeonWaitQueue *queue);
PeonAsyncOp peon_async_checkpoint(void);
PeonAsyncOp peon_async_await_task(PeonSpawnHandle handle);
PeonAsyncOp peon_async_drain_nursery(PeonNursery *nursery);

View File

@@ -971,6 +971,23 @@ static PeonTask *peon_wait_queue_pop(PeonWaitQueue *queue) {
return task;
}
static PeonTask *peon_wait_queue_pop_back(PeonWaitQueue *queue) {
if (queue == NULL || queue->tail == NULL) {
return NULL;
}
PeonTask *task = (PeonTask *)queue->tail;
queue->tail = task->wait_prev;
if (queue->tail == NULL) {
queue->head = NULL;
} else {
((PeonTask *)queue->tail)->wait_next = NULL;
}
task->wait_prev = NULL;
task->wait_next = NULL;
task->wait_queue = NULL;
return task;
}
static bool peon_task_waiter_list_remove(PeonTask **head, PeonTask *task) {
if (head == NULL || task == NULL) {
return false;
@@ -1924,7 +1941,7 @@ void peon_wait_queue_init(PeonWaitQueue *queue) {
queue->tail = NULL;
}
bool peon_wait_queue_wake_one(PeonWaitQueue *queue) {
bool peon_wait_queue_wake_first(PeonWaitQueue *queue) {
if (queue == NULL) {
peon_panic("attempted to wake an invalid wait queue");
}
@@ -1943,17 +1960,43 @@ bool peon_wait_queue_wake_one(PeonWaitQueue *queue) {
return true;
}
bool peon_wait_queue_wake_last(PeonWaitQueue *queue) {
if (queue == NULL) {
peon_panic("attempted to wake an invalid wait queue");
}
PeonTask *task = peon_wait_queue_pop_back(queue);
if (task == NULL) {
return false;
}
if (task->scheduler == NULL) {
peon_panic("attempted to wake a wait-queue task without a scheduler");
}
if (task->scheduler->parked_wait_queue_tasks == 0u) {
peon_panic("async wait-queue waiter count underflow");
}
task->scheduler->parked_wait_queue_tasks -= 1u;
peon_scheduler_push_ready(task->scheduler, task);
return true;
}
int64_t peon_wait_queue_wake_all(PeonWaitQueue *queue) {
if (queue == NULL) {
peon_panic("attempted to wake an invalid wait queue");
}
int64_t woke = 0;
while (peon_wait_queue_wake_one(queue)) {
while (peon_wait_queue_wake_first(queue)) {
woke += 1;
}
return woke;
}
bool peon_wait_queue_is_empty(PeonWaitQueue *queue) {
if (queue == NULL) {
peon_panic("attempted to check size of an invalid wait queue");
}
return queue->head == queue->tail && queue->head == NULL;
}
PeonAsyncOp peon_async_checkpoint(void) {
return (PeonAsyncOp){
.kind = PEON_ASYNC_OP_CHECKPOINT,

View File

@@ -179,7 +179,11 @@ proc scanOperators(tokens: seq[Token]): OperatorScanResult =
proc parseImportPath(tokens: seq[Token], idx: var int): string =
while idx < tokens.len and tokens[idx].kind != Semicolon:
if tokens[idx].lexeme == "..":
if tokens[idx].lexeme == "../":
# The lexer greedily merges ".." and "/" into a single "../" token
result &= "../"
inc(idx)
elif tokens[idx].lexeme == "..":
if idx + 1 >= tokens.len or tokens[idx + 1].lexeme != "/":
break
result &= "../"

View File

@@ -1027,8 +1027,12 @@ proc isCapable*(self: TypeChecker, name: Name, sig: TypeSignature, args: seq[Typ
allowConverters = true): int =
if name.isNil() or name.valueType.isNil() or name.valueType.kind != TypeKind.Function:
return -1
if sig.len() != name.valueType.signature.len() or args.len() != sig.len():
if sig.len() > name.valueType.signature.len():
return -1
if sig.len() < name.valueType.signature.len():
for i in sig.len() ..< name.valueType.signature.len():
if name.valueType.signature[i].default.isNil():
return -1
var score = 0
if name.valueType.isBuiltin and args.len() == 1:
let builtin = self.builtinMagic(name)
@@ -1194,8 +1198,18 @@ proc matchCandidatesResolved*(self: TypeChecker, label: string, pool: seq[Name],
msg &= &"\n - in {relativePath(name.file, getCurrentDir())}:{name.ident.token.line}:{name.ident.token.relPos.start} -> {self.stringify(name.valueType)}"
if name.valueType.kind notin [Function, Structure]:
msg &= ": not callable"
elif sig.len() != name.valueType.signature.len():
elif sig.len() > name.valueType.signature.len():
msg &= &": wrong number of arguments (expected {name.valueType.signature.len()}, got {sig.len()} instead)"
elif sig.len() < name.valueType.signature.len():
var hasMissingRequired = false
var requiredCount = 0
for j in 0 ..< name.valueType.signature.len():
if name.valueType.signature[j].default.isNil():
inc(requiredCount)
if j >= sig.len():
hasMissingRequired = true
if hasMissingRequired:
msg &= &": wrong number of arguments (expected at least {requiredCount}, got {sig.len()} instead)"
else:
for i, arg in sig:
if arg.name != "" and name.valueType.signature[i].name != "" and arg.name != name.valueType.signature[i].name:
@@ -1341,6 +1355,10 @@ proc lowerResolvedCall*(self: TypeChecker, node: CallExpr,
if matched.genericArgs[i].kind == ConstGenericArg:
constBindings[genericBindingKey(parameter.symbol)] = matched.genericArgs[i].value
tc_pragmas.dispatchDelayedPragmas(self, impl)
# Fill in default values for missing trailing arguments
if argExpr.len() < callableType.signature.len():
for i in argExpr.len() ..< callableType.signature.len():
argExpr.add(callableType.signature[i].default)
if impl.valueType.isBuiltin and builtinMagic == MagicBorrow:
return self.builtinBorrowExpr(node, rawArgExpr[0])
if impl.valueType.isBuiltin and builtinMagic == MagicMove:

View File

@@ -1245,11 +1245,14 @@ proc returnStmt(self: Parser): Statement =
proc parseModulePath(self: Parser, stop: openArray[TokenType], context: string): string =
## Parses a module path such as foo/bar or ../foo
while not self.done() and self.peek().kind notin stop:
if self.match(".."):
if not self.check("/"):
self.error(&"expecting '/' after '..' in {context}")
if self.match("../") or self.match(".."):
# The lexer may greedily merge ".." and "/" into a single "../" token,
# or keep them separate depending on what follows. Handle both forms.
if self.peek(-1).lexeme == "..":
if not self.check("/"):
self.error(&"expecting '/' after '..' in {context}")
discard self.step()
result &= "../"
discard self.step()
elif self.match("/"):
self.expect(Identifier, &"expecting identifier after '/' in {context}")
result &= &"/{self.peek(-1).lexeme}"
@@ -1490,8 +1493,8 @@ proc parseGenericConstraint(self: Parser, endToken: TokenType or string): Expres
of "|", "&":
result = newBinaryExpr(result, self.step(), self.parseGenericConstraint(endToken))
result.file = self.file
of ",":
discard # Comma is handled in parseGenerics()
of ",", "=":
discard # Comma is handled in parseGenerics(), = in parseDeclParams()
else:
self.error("invalid type constraint in generic declaration")

View File

@@ -19,6 +19,7 @@ import variants;
type AsyncCompletion*[T] = enum {
# The result of a runAsync() call
Completed {
value: T;
},
@@ -31,7 +32,13 @@ type AsyncClock* = object with ImplicitCopy {
}
type AsyncRuntimeOptions* = object with ImplicitCopy {
# Maximum number of idling threads in the pool used
# for blocking calls. Calls block until there is a
# free thread to pick them up
maxBlockingThreads: int64;
# Maximum number of blocking jobs that can be scheduled
# at once. Calls block until there is enough space on the
# job queue
maxBlockingJobs: int64;
}
@@ -93,32 +100,46 @@ type WaitQueue* = object {
tail: ptr cvoid;
}
type peon_i64 = clonglong; #pragma[importc: "int64_t", header: "<stdint.h>"]
fn peonWaitQueueInitRaw(queue: ptr WaitQueue); #pragma[importc: "peon_wait_queue_init", header: "<peon_runtime.h>", noDecl]
fn WaitQueue*: WaitQueue {
#pragma[constructor]
return WaitQueue(head = unsafe { cast[ptr cvoid](0) }, tail = unsafe { cast[ptr cvoid](0) });
}
fn peonWaitQueueWakeOneRaw(queue: ptr WaitQueue): cbool; #pragma[importc: "peon_wait_queue_wake_one", header: "<peon_runtime.h>", noDecl]
fn peonWaitQueueWakeAllRaw(queue: ptr WaitQueue): peon_i64; #pragma[importc: "peon_wait_queue_wake_all", header: "<peon_runtime.h>", noDecl]
fn initRaw(queue: ptr WaitQueue); #pragma[importc: "peon_wait_queue_init", header: "<peon_runtime.h>", noDecl]
fn wakeFirstRaw(queue: ptr WaitQueue): cbool; #pragma[importc: "peon_wait_queue_wake_first", header: "<peon_runtime.h>", noDecl]
fn wakeLastRaw(queue: ptr WaitQueue): cbool; #pragma[importc: "peon_wait_queue_wake_last", header: "<peon_runtime.h>", noDecl]
fn wakeAllRaw(queue: ptr WaitQueue): clonglong; #pragma[importc: "peon_wait_queue_wake_all", header: "<peon_runtime.h>", noDecl]
fn isEmptyRaw(queue: ptr WaitQueue): cbool; #pragma[importc: "peon_wait_queue_is_empty", header: "<peon_runtime.h>", noDecl]
fn initWaitQueue*(queue: mut lent WaitQueue) {
fn init*(queue: mut lent WaitQueue) {
unsafe {
peonWaitQueueInitRaw(unsafePtr(queue));
initRaw(unsafePtr(queue));
}
}
fn wakeOne*(queue: mut lent WaitQueue): bool {
return toBool(unsafe { peonWaitQueueWakeOneRaw(unsafePtr(queue)) });
fn wakeFirst*(queue: mut lent WaitQueue): bool {
return bool(unsafe { wakeFirstRaw(unsafePtr(queue)) });
}
fn wakeLast*(queue: mut lent WaitQueue): bool {
return bool(unsafe { wakeLastRaw(unsafePtr(queue)) });
}
fn wakeAll*(queue: mut lent WaitQueue): int64 {
return toInt64(unsafe { peonWaitQueueWakeAllRaw(unsafePtr(queue)) });
return int64(unsafe { wakeAllRaw(unsafePtr(queue)) });
}
fn isEmpty*(queue: mut lent WaitQueue): bool {
return bool(unsafe { isEmptyRaw(unsafePtr(queue)) });
}
fn `clone=`*(src: WaitQueue): WaitQueue {
var copied = defaultClone();
initWaitQueue(mut &copied);
copied.init();
return copied;
}

View File

@@ -13,7 +13,7 @@ import builtins/math;
import arrays;
import ranges;
import strings;
import sync;
import sync/events;
import net;
@@ -32,5 +32,5 @@ export math;
export arrays;
export ranges;
export strings;
export sync;
export events;
export net;

View File

@@ -12,57 +12,53 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import builtins/values;
import builtins/async_runtime;
import builtins/misc;
import cinterop;
import ../builtins/values;
import ../builtins/async_runtime;
import ../builtins/misc;
import ../cinterop;
type Event* = object {
type Event* = object with Copy {
#pragma[noWarn: RawPointerLeak]
# We silence the warning because even though
# WaitQueue does contain raw pointers it does
# not own them, so there is nothing that needs
# freeing from the peon side: the C runtime owns
# the tasks! TODO: Should we have a custom destructor
# to wake all tasks?
waiters: WaitQueue;
signaled: bool;
}
fn init*(self: mut lent Event) {
initWaitQueue(self.waiters);
self.signaled = false;
}
fn Event*(): Event {
fn Event*: Event {
#pragma[constructor]
return Event(waiters = WaitQueue(head = unsafe { cast[ptr cvoid](0) },
tail = unsafe { cast[ptr cvoid](0) }),
signaled = false);
}
fn newEvent*(): Event {
return Event();
return Event(waiters=WaitQueue(), signaled=false);
}
fn `clone=`*(src: Event): Event {
var copied = defaultClone();
initWaitQueue(mut &copied.waiters);
copied.waiters.init();
return copied;
}
fn `destroy=`*(value: mut lent Event) {
defaultDestroy();
}
fn set*(self: mut lent Event) {
if self.signaled {
return;
}
self.signaled = true;
wakeAll(self.waiters);
self.waiters.wakeAll();
}
fn reset*(self: mut lent Event) {
if not self.waiters.isEmpty() {
panic("attempted to reset event with some tasks awaiting on it!");
}
self.signaled = false;
self.waiters.init();
}
@@ -71,13 +67,13 @@ fn isSet*(self: lent Event): bool {
}
fn waitQueue(self: mut lent Event): mut lent WaitQueue {
return self.waiters;
}
async fn wait*(self: mut lent Event) {
while not isSet(self) {
await park(self.waitQueue());
if self.isSet() {
# Every await must be a checkpoint!
await checkpoint();
return;
}
while not self.isSet() {
await self.waiters.park();
}
}

View File

@@ -0,0 +1,69 @@
import events;
import ../cinterop;
import ../builtins/seq;
import ../builtins/misc;
import ../builtins/values;
import ../builtins/async_runtime;
type FIFOQueue*[T] = object {
# A first-in, first-out asynchronous queue. If
# a max size is defined, pushing elements once
# the queue is full causes the caller to block
# until space is made on it by some other task
# popping items off the queue. If no items are
# on the queue, attempting to pop one off will
# block the caller until some other task pushes
# a value back on the queue
getters: WaitQueue;
putters: WaitQueue;
maxSize: int64;
data: seq[T];
}
fn FIFOQueue*[T](maxSize: int64 = 0): FIFOQueue[T] {
# Constructs a new FIFO queue of the provided maximum
# size. A value of 0 for maxSize indicates the queue
# grows without bounds
return FIFOQueue(getters=WaitQueue(),
putters=WaitQueue(),
maxSize=maxSize,
data=newSeqOfCap[T](maxSize)
);
}
fn len*[T](self: FIFOQueue[T]): int64 {
return self.data.len();
}
fn high*[T](self: FIFOQueue[T]): int64 {
return self.data.high();
}
async fn push*[T](self: FIFOQueue[T], v: T) {
while self.maxSize > 0 and self.len() == self.maxSize {
await self.putters.park();
}
if not self.getters.isEmpty() {
self.getters.wakeFirst();
}
self.data.append(v);
# Holy rule of peon's async stdlib: every await is ALWAYS a checkpoint
await checkpoint();
}
async fn pop*[T](self: FIFOQueue[T]): T {
while self.len() == 0 {
await self.getters.park();
}
if not self.putters.isEmpty() {
self.putters.wakeFirst();
}
await checkpoint();
return self.data.pop();
}

View File

@@ -260,7 +260,7 @@ unsafe fn unsafePtr[T](value: mut lent T): ptr T {
fn peonWaitQueueInitRaw(queue: ptr WaitQueue); #pragma[importc: "peon_wait_queue_init", header: "<peon_runtime.h>", noDecl]
fn initWaitQueue(queue: mut lent WaitQueue) {
fn init(queue: mut lent WaitQueue) {
unsafe {
peonWaitQueueInitRaw(unsafePtr(queue));
}