v3.0.0 #44
minirang
announced in
Announcements
v3.0.0
#44
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Description
Pure-function hardening: the restriction now propagates through the call graph. Previously
pureonly checked a function's own body, soset pure func f() do: g() endwheregtouches a global was allowed —fnever touched a global directly.checkPureCallAllowed()now runs at every call site and rejects a pure caller invoking a non-pure user-defined function or closure, with a new error,PureFunctionImpureCall(E4029). Native/DLC functions remain exempt (they can't reach a CuffScript global through this route at all). Behavior change:tests/cases/pure_functions.cuff's old "pure calling non-pure is fine" example no longer holds; it now lives intests/errors/pure_func_call_impure.cuffas anE4029case.Fixed: a real bug in
change x to global.Environment::resolve()checked whether a name was bridged to global before checking for an actual local at each scope level. Once bridged, any later local redeclaration of the same name in that call — most visibly aloop repeat x to 1 ~ 3reusing a bridgedxas its own loop counter — became permanently invisible: every read silently returned the stale global instead of the loop's own value, with no error. Fixed by checking local-first everywhere, matching the "innermost declaration wins" rule the rest of the language already follows. Same bug also affected thepureglobal-access check, which could previously misreport a shadowed local as forbidden global access.Reserved words can now be used as identifiers.
add,count,find,split,replace,match,in,by,not,globalcan now be used as variable, function, parameter, and loop-variable names, and read back in expressions.match/find/replace/split/count(which have their own expression syntax) use one token of lookahead to tell "the construct" from "just a name" apart, without breaking the ability to call or index something with one of those names.New: caret (
^) marker in error messages. Errors now show the source line with a^under the exact column (codepoint-aligned, so multibyte UTF-8 text earlier on the line — Korean, emoji, ... — doesn't throw off the alignment).New:
DLC:filesystem. Real local file access:file_exist,file_size,file_read,file_readlines,file_write,file_add,file_remove. Every path is confined to the exact same sandbox rootuse ... frommodule imports already use — an absolute path or a../-escape is rejected with a new error,FilesystemAccessDenied(E5008), before touching the filesystem. Symlinks pointing outside the root are also rejected (verified by hand). New host controls, mirroringDLC:network's existing shape:CuffEngine::Options::filesystemEnabled/--no-filesystem. SeeSECURITY.md.DLC function naming convention:
library_verb. Every DLC function name now carries its library as a prefix —sqrt→math_sqrt,upper→str_upper,sort→list_sort,get/post→network_get/network_post, and so on — so a script can tell which library a call came from without cross-referencing everyuseline, and two libraries can no longer silently shadow each other's bare names in the shared native-function table. Exceptions:length/contains/index_of(intentionally polymorphic across str/list/map) andto_json/from_json/to_number/to_str/to_boolean(already self-describing). This is a breaking rename — every example, test, and doc in this repository was updated; external scripts need the same mechanical rename per function (full mapping indocs/IMPLEMENTATION_NOTES.md§9/§31).loop matchremoved. It was implemented identically toloop while(same condition-recheck-every-iteration code path, same parsing) — a complete, confusing duplicate with no functional difference and no use anywhere in this repository.loop while [condition] do: ... endcovers the same case.LoopStmt::LoopKindnow has justRepeat/While.New: nested functions / closures.
set funcinside another function's body no longer errors — it creates a closure: a first-class value that can be assigned to a variable (set func NAME to EXPR, a new type-keyword use), passed as an argument, returned, and called by name, including recursively. Capture is by value, once, at definition time — not shared upvalues: a captured number/str/boolean is independent from that point on (a closure does not accumulate state across separate calls — the classic "counter closure" pattern does not work here), while a captured list/map is still shared by reference, exactly like an ordinary function argument. A closure created inside apurefunction is automatically forced pure regardless of its ownpurekeyword, closing the same escape hatch the call-graph hardening above closes for named functions. Top-level function declarations are unaffected (still not values, still the fast named registry). Nestedasyncfunctions are explicitly rejected for now with a clear error (NestedAsyncFunctionNotSupported, reusing error codeE4018) rather than silently running wrong or dropping their capture. Calling something that isn't a function now says so specifically ('x' is a number, not a function) instead of a generic "undefined function".Async concurrency: reasoned decision not to add OS threads. Evaluated a full design (GIL-style lock + worker-thread pool, releasing the lock only around a queued task's blocking network I/O, with per-call-stack state made thread-local so concurrently-running tasks can't corrupt each other's bookkeeping) and chose not to ship it. Genuine concurrent execution needs either OS threads or turning the evaluator into a resumable state machine; the rewrite is out of scope, and the threading approach is a permanent complexity and correctness tax on every future change to the interpreter, for a feature (overlapping network I/O) most scripts will never exercise. No code changed for this item —
f()on anasyncfunction still queues it for after the top-level script's synchronous code finishes (strict FIFO order, self-queued tasks included);await f()still runs it immediately and synchronously. This is cooperative scheduling, not concurrency, and is now documented as such.Added error codes:
E4029(PureFunctionImpureCall),E5008(FilesystemAccessDenied). Reused (repurposed, previously unused after the closures change):E4018, nowNestedAsyncFunctionNotSupportedinstead of the old blanketNestedFunctionNotSupported.Fixed: a flaky test-runner bug, unrelated to the engine.
tests/run.sh's error-code check piped a captured error message intogrep -qunderbash -o pipefail;grep -qexits at its first match, which can make the upstreamechodie ofSIGPIPE— pipefail then reports the whole pipeline as failed even though the message matched. Measured at roughly 1 false failure in 1,500 runs on a ~450-byte message, worse on longer ones. Switched to a here-string (grep -q "..." <<<"$actual"), which has no pipe to race; confirmed at 0 failures in 6,000 stress-runs of the old failure pattern.Tests: 4 new script cases (
closures,dlc_filesystem,global_bridge_shadowing,reserved_words_as_identifiers), 7 new error cases, and a new C++ unit test file (error_snippet_test.cpp, 3 checks for the caret marker). The suite now has 119 checks (was 106).Verification. Full suite passes clean under AddressSanitizer + UBSan, with one caveat:
tests/cases/value_semantics.cuffintentionally builds circular list/map structures (add a to a), andshared_ptrreference cycles never reach a zero refcount, so LeakSanitizer correctly flags them — this is a pre-existing architectural tradeoff (predates this change; the same construct leaked before it too), not a new bug, and every other script in the suite is leak-clean including all new closure and filesystem code (closure self-recursion in particular was specifically checked, since it could easily have introduced its own reference cycle — it doesn't). Also run clean:-pedantic, and libstdc++'s debug-iterator mode (_GLIBCXX_DEBUG). One real, if minor, regression was caught and fixed during ASan verification: identifier-token handling inlined into the parser's hot recursive-descent path enlarged that function's stack frame enough to measurably reduce how deeply((((...))))-style nesting could go before the existing stack-depth guard trips (462 vs. 483 levels under ASan's larger frames) — moved into a dedicatednoinlinehelper, which recovered it (506 vs. 483, so no worse than before, if anything slightly better from the refactor). Windows: not cross-compiled this round (nomingw-w64toolchain available in this environment); the new code is all standard C++17 +<filesystem>(already an existing, working dependency in this codebase's module-loader), introduces no new platform-specific APIs, and follows the same patterns as the existing, already-Windows-tested code — reviewed by hand rather than compiled. macOS: not cross-compiled or reviewed against real Windows/macOS toolchains either — same caveat as prior releases' entries for this project.This discussion was created from the release v3.0.0.
All reactions