Skip to content

Known Limitations ​

Wyn is honest about what works and what doesn't, and this page is re-run rather than carried forward: each entry is checked against the current binary, and anything that no longer reproduces is moved to "no longer limitations" with the evidence.

Last re-run: v1.23.0-rc, on macOS arm64 and Linux arm64. Four entries moved out as a result, each using the example the entry itself had used to demonstrate the bug: print of a HashSet, println of a struct with a nested-struct or array field, unknown methods on a builtin namespace (now a checker error, not a build failure), and the regex shorthand classes \d/\w/\s — which also turned out to support backreferences. The previous re-run, against v1.22.0, had moved three: printing a map, printing none, and parallel { } overlapping only two CPU-bound branches.

Four stale entries in one release is the argument for re-running the list instead of editing around it. Entries that are still true were re-confirmed the same way, not assumed: string.len() counting bytes, Math.checked_add panicking rather than returning a Result, a top-level lambda failing to build, and Regex.find_all/split returning newline-joined strings.

Runtime ​

  • A wrong argument type on a stdlib call is not caught, and can segfault. wyn check does not verify builtin argument types, so passing an int where a string is expected compiles into a pointer cast:

    wyn
    x = File.read(7)      // check: no errors. build: OK. run: SIGSEGV (exit 139)

    Measured across 18 deliberately wrong calls: none was caught by wyn check, 14 compiled, and every compiled one crashed. Crypto.sha256(3), Log.info(5), Url.encode(3), Http.get(42) and Json.parse(9) all behave this way. Arity is unchecked too - Uuid.v4("x") compiles although the function takes no arguments. This is the most serious item on this page: pass the types the API reference states, because nothing else will check them for you. Filed as issue #475.

    Not every builtin is in this state. The ones registered with a precise signature — Http.status() and Http.error() among them — do check both arity and argument types, and give an ordinary "wrong number of arguments" error.

  • A lambda assigned at top level (script mode) fails to build. Inside fn main() both lambda spellings work; at the top level of a script the variable is typed as an integer and the call fails with called object type 'long long' is not a function. Put lambdas inside a function.

  • spawn on a closure returns the wrong answer, silently. A closure called directly returns its value; the same closure spawned returns 0. wyn check passes, the build succeeds, and the process exits 0:

    wyn
    var n = 5
    g = (() => n * 2)
    println(g())              // 10
    f = spawn (() => n * 2)
    println(await f)          // 0  <-- wrong

    spawn needs a function pointer, and a closure's captured environment is not carried across. Until this is fixed, pass captured values as explicit parameters to a named function and spawn that. This is the most serious item on this page because nothing warns you.

  • parallel { } leaves some branch shapes sequential, and says which. A method call on a value, a non-scalar argument, a non-word result, or more than eight arguments is refused by name rather than silently run inline. Everything else - a bare call, a declaration from a call, assignment to an outer variable, an explicit spawn, and a namespaced builtin in either the . or the :: spelling - is dispatched. await on a spawned builtin yields 0.

  • Recursive spawn - spawn inside recursive functions works reliably up to ~28K outstanding tasks (fib(22) verified on v1.21.0). Beyond that, the thread pool can deadlock. Use sequential recursion for deep trees, spawn for parallel leaf work.

  • string.len() returns bytes - UTF-8 byte count, not character count. "héllo".len() is 6 and "日本語".len() is 9.

  • Math.checked_add() panics, it does not return a Result. It is a guard against silently wrapping, not a recoverable check: Math.checked_add(9223372036854775807, 1) aborts with panic: integer overflow in checked_add(...).

No Longer Limitations ​

  • print of a HashSet prints a pointer address - fixed. s = {:"a"} / print(s) now prints {:"a"}, and print({"a": 1}) prints {"a": 1}, so maps and sets format alike. Re-verified on v1.23.0-rc.

  • println(v) on a struct with a nested-struct or array field fails to build - fixed. println(o) on Outer { i: Inner { n: 1 }, xs: [1, 2] } prints Outer { i: Inner { n: 1 }, xs: [1, 2] }. Interpolation and the bare call now agree, so the workaround this entry recommended is no longer needed. Re-verified on v1.23.0-rc.

  • Unknown methods on a builtin namespace pass wyn check and fail to build - fixed, and it is now a checker error with a Wyn-level message. The exact case this entry named: Time.millis() gives Error at line 1: unknown method 'Time.millis' on namespace 'Time' and a help line pointing at the namespace's docs, at wyn check time (exit 1). Re-verified on v1.23.0-rc.

  • Regex shorthand classes \d, \w, \s are unsupported and fail silently - fixed, on both platforms tested. Regex.match("a1", "\\d") is true, Regex.match("abc", "\\d") is false, and the example this entry used to prove the bug now behaves correctly: Regex.replace("ab123cd", "\\d+", "N") is abNcd, not the letters rewritten. \w and \s work in match, replace, split and find_all. Backreferences also work - (ab)\1 matches abab and does not match abcd. Re-verified on v1.23.0-rc on macOS arm64 and Linux arm64.

  • println of a map or of none prints a pointer address. Fixed in v1.22.print(m) on a map gives {"a": 1}, interpolation gives the same, an Option variable prints Some(5) and an empty one prints none. A HashSet still prints a pointer, which is why that half stays on the list above.

  • parallel { } overlaps only two CPU-bound branches; three or four take two dispatch rounds. Fixed in v1.22 - and there were never any dispatch rounds. Four branches with distinct arguments now finish in 1.04x of one branch (5,664µs for one, 5,881µs for four). The original figure came from a fixture that gave every branch the same argument; because the function was pure, the C compiler collapsed the identical sibling calls into one, so two branches measured like perfect overlap while dispatching nothing, and adding more branches looked like a second round.

  • A bare Time.sleep(200) in a parallel { } block runs inline. Fixed in v1.22. Eight branches of a 200ms sleep take 210ms against a 204ms single-branch baseline; on v1.21.0 the same program took 1,649ms. Wrapping the call in a one-line user function is no longer necessary.

  • .len() is O(n) on some StringBuilder results. Fixed in v1.22. The RC header's cached length is now filled on a read that misses, not only by whichever constructor remembered to set it, so every string is O(1) after its first .len() - about 1.7ns regardless of length. On a 100,000-character builder result, 100,000 calls went from 242ms to 0.16ms. The advice to hoist .len() out of a loop no longer applies. Filling the cache on a read initially cost a literal receiver 1.75x, because a literal lives in read-only memory and can never hold a cached length, so it paid for the lookup forever; that is measured and fixed too - a literal is back to its original 1.5ns.

  • Integer overflow - silent wrap-around. As of v1.21.0, wrapping is defined. Wyn compiles with -fwrapv, so signed integer overflow wraps two's complement and is guaranteed to: 9223372036854775807 + 1 is -9223372036854775808, every time, at every optimisation level. Previously the optimiser was entitled to assume overflow could not happen, which is how wrapping code turns into deleted comparisons. It costs nothing measurable - fib(35) is unchanged. Use Math.checked_add() if you want the overflow to abort instead.

  • Sort performance - slower than Go. Re-measured on v1.21.0 with both sides optimised: [int].sort() does 1M ints in 73ms against Go's sort.Ints at 83ms, and 100K in 6.3ms against 7.0ms. The old figures (124ms vs 101ms) compared a Wyn dev build against go build, which optimises by default. A dev build really does take 110ms, so use --release for anything you intend to time.

Compile Time ​

  • Compile speed - wyn build takes ~356ms (dev) or ~1.5s (release) for hello world, and ~660ms / ~3.0s on 5,000 lines of realistic code. wyn check is ~9ms on hello world - but that is almost entirely process startup (wyn version alone is ~8.4ms), and checking rises to ~17ms at 1,000 lines and ~72ms at 5,000. The bottleneck for builds is the C compiler (clang/gcc), not Wyn's codegen. Go's purpose-built compiler is faster (go build hello world, forcing a real link: 189ms).

Platform Notes ​

  • Windows cross-compilation - not yet supported from macOS. Linux x64/ARM64 work.

  • Regex: lookaround and capture-group extraction are unsupported. Regex.match("abc", "a(?=b)") is false, and Regex.match returns a plain bool - there is no match object and no .group(n), so you cannot pull a captured substring out. Everything else tested works: ., classes, *, +, ?, {n}, {n,m}, alternation, ^/$, the shorthand classes \d/\w/\s, and backreferences.

    Two engines, one namespace. Regex.find lowers to a POSIX regcomp/regexec path (#ifndef _WIN32), while match/replace/find_all/split use the built-in NFA, and on Windows Regex.find falls back to the NFA as well. They agreed on every case tested here, but they are separate implementations with separate notions of an invalid pattern, so a pattern that is rejected by one may not be by the other.

  • Regex.find_all and Regex.split return a newline-joined string, not an array - as does Db.query (rows by \n, columns by |). Iterate with .trim().split("\n").

  • Mobile - system()/popen() return stubs on iOS/Android.

  • WASM - not yet supported. Runtime has POSIX dependencies that need #ifdef guards.

See Also ​

MIT License - v1.22.0