Port scanning in 11 lines of concurrent Wyn
One of Wyn's design principles is "concurrency should be a verb, not a thesis." Here's what that looks like in practice.
The problem
Scan 11 ports on localhost. Sequentially, each nc -z takes up to 1 second to timeout. That's 11 seconds for a simple port scan.
The Wyn way
fn scan(host: string, port: int, results: [int], i: int) {
var code = System.exec_code("nc -z -w1 ${host} ${port} 2>/dev/null")
results[i] = code
}
fn main() {
var ports = [22, 80, 443, 3000, 3306, 5432, 6379, 8080, 8443, 9090, 27017]
var results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
for i in 0..11 {
spawn scan("localhost", ports[i], results, i)
}
Time.sleep(2000)
for i in 0..11 {
var status = "closed"
if results[i] == 0 { status = "OPEN" }
println(" ${ports[i]}: ${status}")
}
}11 ports scanned concurrently. Finishes in ~1 second instead of ~11. The spawn keyword is the entire concurrency story.
What's actually happening
spawn creates a lightweight coroutine — a green thread with a 16KB stack, scheduled across OS threads by a work-stealing scheduler. It's the same model as Go goroutines, but the syntax is simpler.
There's no WaitGroup. No sync.Mutex. No channel. You spawn work, the results land in a shared array, and you read them after a sleep. For this use case, that's all you need.
For more complex coordination, Wyn has await (for getting return values from spawns) and Task (for atomic shared state). But the point is you don't need them for the common case.
The Go equivalent
func scan(host string, port int, results []int, i int, wg *sync.WaitGroup) {
defer wg.Done()
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), time.Second)
if err != nil {
results[i] = 1
return
}
conn.Close()
results[i] = 0
}
func main() {
ports := []int{22, 80, 443, 3000, 3306, 5432, 6379, 8080, 8443, 9090, 27017}
results := make([]int, len(ports))
var wg sync.WaitGroup
for i, port := range ports {
wg.Add(1)
go scan("localhost", port, results, i, &wg)
}
wg.Wait()
for i, port := range ports {
status := "closed"
if results[i] == 0 { status = "OPEN" }
fmt.Printf(" %d: %s\n", port, status)
}
}More code. sync.WaitGroup boilerplate. defer wg.Done(). fmt.Sprintf. It works fine, but it's more ceremony for the same result.
Go's concurrency is powerful. Wyn's is simpler. That's the tradeoff we chose.
Performance
On an M4 Mac, spawning 10,000 coroutines takes about 12ms. Each coroutine uses ~18KB of memory (16KB stack + overhead). That's enough for most real-world concurrent workloads.
Go's goroutines start at 2KB and grow, so Go wins on memory density for massive fan-out. But for the 99% case — scanning ports, making HTTP requests, processing files in parallel — Wyn's model is more than enough.
The philosophy
We didn't invent anything new here. Green threads have been around for decades. What we did is make the syntax disappear:
spawn do_work(args)That's it. No imports, no setup, no teardown. Concurrency is a verb.