diff --git a/build.bat b/build.bat index 32c2ae658..017bdc09c 100644 --- a/build.bat +++ b/build.bat @@ -130,20 +130,22 @@ set linker_settings=%libs% %odin_res% %linker_flags% del *.pdb > NUL 2> NUL del *.ilk > NUL 2> NUL -rc %rc_flags% %odin_rc% +rem rc %rc_flags% %odin_rc% cl %compiler_settings% "src\main.cpp" "src\libtommath.cpp" /link %linker_settings% -OUT:%exe_name% if %errorlevel% neq 0 goto end_of_build -mt -nologo -inputresource:%exe_name%;#1 -manifest misc\odin.manifest -outputresource:%exe_name%;#1 -validate_manifest -identity:"odin, processorArchitecture=amd64, version=%odin_version_full%, type=win32" -if %errorlevel% neq 0 goto end_of_build +rem mt -nologo -inputresource:%exe_name%;#1 -manifest misc\odin.manifest -outputresource:%exe_name%;#1 -validate_manifest -identity:"odin, processorArchitecture=amd64, version=%odin_version_full%, type=win32" +rem if %errorlevel% neq 0 goto end_of_build -call build_vendor.bat -if %errorlevel% neq 0 goto end_of_build +rem call build_vendor.bat +rem if %errorlevel% neq 0 goto end_of_build rem If the demo doesn't run for you and your CPU is more than a decade old, try -microarch:native -if %release_mode% EQU 0 odin run examples/demo -vet -strict-style -resource:examples/demo/demo.rc -- Hellope World +rem if %release_mode% EQU 0 odin run examples/demo -vet -strict-style -resource:examples/demo/demo.rc -- Hellope World + +W:\Odin\odin run examples/bug rem Many non-compiler devs seem to run debug build but don't realize. -if %release_mode% EQU 0 echo: & echo Debug compiler built. Note: run "build.bat release" if you want a faster, release mode compiler. +rem if %release_mode% EQU 0 echo: & echo Debug compiler built. Note: run "build.bat release" if you want a faster, release mode compiler. del *.obj > NUL 2> NUL diff --git a/core/compress/zlib/zlib.odin b/core/compress/zlib/zlib.odin index b9189c1f1..e484a4958 100644 --- a/core/compress/zlib/zlib.odin +++ b/core/compress/zlib/zlib.odin @@ -1,4 +1,4 @@ -#+vet !using-param +#+feature using-stmt package compress_zlib /* diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin index 25e0228b5..6a30d135a 100644 --- a/core/image/netpbm/netpbm.odin +++ b/core/image/netpbm/netpbm.odin @@ -1,4 +1,3 @@ -#+vet !using-stmt package netpbm import "core:bytes" @@ -73,8 +72,7 @@ save_to_buffer :: proc(img: ^Image, custom_info: Info = {}, allocator := context } } - // using info so we can just talk about the header - using info + header := &info.header // validation if header.format in (PBM + PGM + Formats{.Pf}) && img.channels != 1 \ @@ -664,14 +662,14 @@ autoselect_pbm_format_from_image :: proc(img: ^Image, prefer_binary := true, for ASCII :: Formats{.P1, .P2, .P3} */ - using res.header + h := &res.header - width = img.width - height = img.height - channels = img.channels - depth = img.depth - maxval = 255 if img.depth == 8 else 65535 - little_endian = true if ODIN_ENDIAN == .Little else false + h.width = img.width + h.height = img.height + h.channels = img.channels + h.depth = img.depth + h.maxval = 255 if img.depth == 8 else 65535 + h.little_endian = ODIN_ENDIAN == .Little // Assume we'll find a suitable format ok = true @@ -680,37 +678,37 @@ autoselect_pbm_format_from_image :: proc(img: ^Image, prefer_binary := true, for case 1: // Must be Portable Float Map if img.depth == 32 { - format = .Pf + h.format = .Pf return } if force_black_and_white { // Portable Bit Map - format = .P4 if prefer_binary else .P1 - maxval = 1 + h.format = .P4 if prefer_binary else .P1 + h.maxval = 1 return } else { // Portable Gray Map - format = .P5 if prefer_binary else .P2 + h.format = .P5 if prefer_binary else .P2 return } case 3: // Must be Portable Float Map if img.depth == 32 { - format = .PF + h.format = .PF return } // Portable Pixel Map - format = .P6 if prefer_binary else .P3 + h.format = .P6 if prefer_binary else .P3 return case: // Portable Arbitrary Map if img.depth == 8 || img.depth == 16 { - format = .P7 - scale = pfm_scale + h.format = .P7 + h.scale = pfm_scale return } } diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 1a7240bad..0170d3168 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -1,4 +1,4 @@ -#+vet !using-stmt +#+feature using-stmt package png /* diff --git a/core/sys/windows/gdi32.odin b/core/sys/windows/gdi32.odin index 756ec5011..ad2243b56 100644 --- a/core/sys/windows/gdi32.odin +++ b/core/sys/windows/gdi32.odin @@ -34,7 +34,6 @@ foreign gdi32 { SetDCBrushColor :: proc(hdc: HDC, color: COLORREF) -> COLORREF --- GetDCBrushColor :: proc(hdc: HDC) -> COLORREF --- PatBlt :: proc(hdc: HDC, x, y, w, h: INT, rop: DWORD) -> BOOL --- - Rectangle :: proc(hdc: HDC, left, top, right, bottom: INT) -> BOOL --- CreateFontW :: proc(cHeight, cWidth, cEscapement, cOrientation, cWeight: INT, bItalic, bUnderline, bStrikeOut, iCharSet, iOutPrecision: DWORD, iClipPrecision, iQuality, iPitchAndFamily: DWORD, pszFaceName: LPCWSTR) -> HFONT --- CreateFontIndirectW :: proc(lplf: ^LOGFONTW) -> HFONT --- @@ -70,12 +69,20 @@ foreign gdi32 { RealizePalette :: proc(hdc: HDC) -> UINT --- SetTextColor :: proc(hdc: HDC, color: COLORREF) -> COLORREF --- - RoundRect :: proc(hdc: HDC, left: INT, top: INT, right: INT, bottom: INT, width: INT, height: INT) -> BOOL --- SetPixel :: proc(hdc: HDC, x: INT, y: INT, color: COLORREF) -> COLORREF --- GdiTransparentBlt :: proc(hdcDest: HDC, xoriginDest, yoriginDest, wDest, hDest: INT, hdcSrc: HDC, xoriginSrc, yoriginSrc, wSrc, hSrc: INT, crTransparent: UINT) -> BOOL --- GdiGradientFill :: proc(hdc: HDC, pVertex: PTRIVERTEX, nVertex: ULONG, pMesh: PVOID, nCount: ULONG, ulMode: ULONG) -> BOOL --- GdiAlphaBlend :: proc(hdcDest: HDC, xoriginDest, yoriginDest, wDest, hDest: INT, hdcSrc: HDC, xoriginSrc, yoriginSrc, wSrc, hSrc: INT, ftn: BLENDFUNCTION) -> BOOL --- + + // Filled Shape Functions + Rectangle :: proc(hdc: HDC, left, top, right, bottom: c_int) -> BOOL --- + Ellipse :: proc(hdc: HDC, left, top, right, bottom: c_int) -> BOOL --- + RoundRect :: proc(hdc: HDC, left, top, right, bottom, width, height: c_int) -> BOOL --- + Pie :: proc(hdc: HDC, left, right, top, bottom, xr1, yr1, xr2, yr2: c_int) -> BOOL --- + Chord :: proc(hdc: HDC, x1, y1, x2, y2, x3, y3, x4, y4: c_int) -> BOOL --- + Polygon :: proc(hdc: HDC, apt: [^]POINT, cpt: c_int) -> BOOL --- + PolyPolygon :: proc(hdc: HDC, apt: [^]POINT, asz: [^]c_int, csz: c_int) -> BOOL --- } @(require_results) diff --git a/core/sys/windows/user32.odin b/core/sys/windows/user32.odin index cd3efdeb7..580c09b45 100644 --- a/core/sys/windows/user32.odin +++ b/core/sys/windows/user32.odin @@ -58,6 +58,7 @@ foreign user32 { IsZoomed :: proc(hwnd: HWND) -> BOOL --- BringWindowToTop :: proc(hWnd: HWND) -> BOOL --- GetTopWindow :: proc(hWnd: HWND) -> HWND --- + GetWindow :: proc(hwnd: HWND, uCmd: UINT) -> HWND --- SetForegroundWindow :: proc(hWnd: HWND) -> BOOL --- GetForegroundWindow :: proc() -> HWND --- GetDesktopWindow :: proc() -> HWND --- @@ -279,6 +280,7 @@ foreign user32 { FillRect :: proc(hDC: HDC, lprc: ^RECT, hbr: HBRUSH) -> c_int --- FrameRect :: proc(hDC: HDC, lprc: ^RECT, hbr: HBRUSH) -> c_int --- + InvertRect :: proc(hDC: HDC, lprc: ^RECT) -> BOOL --- EqualRect :: proc(lprc1, lprc2: ^RECT) -> BOOL --- OffsetRect :: proc(lprc1: ^RECT, dx, dy: INT) -> BOOL --- InflateRect :: proc(lprc1: ^RECT, dx, dy: INT) -> BOOL --- @@ -939,3 +941,13 @@ ESB_DISABLE_UP :: 0x0001 ESB_DISABLE_DOWN :: 0x0002 ESB_DISABLE_LTUP :: ESB_DISABLE_LEFT ESB_DISABLE_RTDN :: ESB_DISABLE_RIGHT + +// Command constants for GetWindow +GW_HWNDFIRST :: 0 +GW_HWNDLAST :: 1 +GW_HWNDNEXT :: 2 +GW_HWNDPREV :: 3 +GW_OWNER :: 4 +GW_CHILD :: 5 +GW_ENABLEDPOPUP :: 6 +GW_MAX :: 6 diff --git a/core/sys/windows/ux_theme.odin b/core/sys/windows/ux_theme.odin index 4ce126c73..e19800246 100644 --- a/core/sys/windows/ux_theme.odin +++ b/core/sys/windows/ux_theme.odin @@ -14,5 +14,62 @@ PMARGINS :: ^MARGINS @(default_calling_convention="system") foreign uxtheme { IsThemeActive :: proc() -> BOOL --- + GetWindowTheme :: proc(hwnd: HWND) -> HTHEME --- SetWindowTheme :: proc(hWnd: HWND, pszSubAppName, pszSubIdList: LPCWSTR) -> HRESULT --- + + // Buffered painting and buffered animation + BufferedPaintInit :: proc() -> HRESULT --- + BufferedPaintUnInit :: proc() -> HRESULT --- + + BeginBufferedPaint :: proc(hdcTarget: HDC, prcTarget: ^RECT, dwFormat: BP_BUFFERFORMAT, pPaintParams: ^BP_PAINTPARAMS, phdc: ^HDC) -> HPAINTBUFFER --- + EndBufferedPaint :: proc(hBufferedPaint: HPAINTBUFFER, fUpdateTarget: BOOL) -> HRESULT --- + + GetBufferedPaintTargetRect :: proc(hBufferedPaint: HPAINTBUFFER, prc: ^RECT) -> HRESULT --- + GetBufferedPaintTargetDC :: proc(hBufferedPaint: HPAINTBUFFER) -> HDC --- + GetBufferedPaintDC :: proc(hBufferedPaint: HPAINTBUFFER) -> HDC --- + GetBufferedPaintBits :: proc(hBufferedPaint, ppbBuffer: ^[^]RGBQUAD, pcxRow: ^c_int) -> HRESULT --- + + BufferedPaintClear :: proc(hBufferedPaint: HPAINTBUFFER, prc: ^RECT) -> HRESULT --- + BufferedPaintSetAlpha :: proc(hBufferedPaint: HPAINTBUFFER, prc: ^RECT, alpha: BYTE) -> HRESULT --- + + BufferedPaintStopAllAnimations :: proc(hwnd: HWND) -> HRESULT --- + BeginBufferedAnimation :: proc(hwnd: HWND, hdcTarget: HDC, prcTarget: ^RECT, dwFormat: BP_BUFFERFORMAT, pPaintParams: ^BP_PAINTPARAMS, pAnimationParams: ^BP_ANIMATIONPARAMS, phdcFrom: ^HDC, phdcTo: ^HDC) -> HANIMATIONBUFFER --- + BufferedPaintRenderAnimation :: proc(hwnd: HWND, hdcTarget: HDC) -> BOOL --- +} + +HTHEME :: distinct HANDLE +HPAINTBUFFER :: distinct HANDLE +HANIMATIONBUFFER :: distinct HANDLE + +BP_BUFFERFORMAT :: enum c_int { + BPBF_COMPATIBLEBITMAP, + BPBF_DIB, + BPBF_TOPDOWNDIB, + BPBF_TOPDOWNMONODIB, +} + +BP_ANIMATIONSTYLE :: enum c_int { + BPAS_NONE, + BPAS_LINEAR, + BPAS_CUBIC, + BPAS_SINE, +} + +// Constants for BP_PAINTPARAMS.dwFlags +BPPF_ERASE :: 0x0001 +BPPF_NOCLIP :: 0x0002 +BPPF_NONCLIENT :: 0x0004 + +BP_ANIMATIONPARAMS :: struct { + cbSize: DWORD, + dwFlags: DWORD, + style: BP_ANIMATIONSTYLE, + dwDuration: DWORD, +} + +BP_PAINTPARAMS :: struct { + cbSize: DWORD, + dwFlags: DWORD, + prcExclude: ^RECT, + pBlendFunction: ^BLENDFUNCTION, } diff --git a/core/testing/signal_handler_libc.odin b/core/testing/signal_handler_libc.odin index a1441f29d..74608bb48 100644 --- a/core/testing/signal_handler_libc.odin +++ b/core/testing/signal_handler_libc.odin @@ -1,5 +1,5 @@ #+private -#+build windows, linux, darwin, freebsd, openbsd, netbsd, haiku +#+build linux, darwin, freebsd, openbsd, netbsd, haiku package testing /* @@ -8,6 +8,7 @@ package testing List of contributors: Feoramund: Total rewrite. + blob1807: Windows Win32 API rewrite. */ import "base:intrinsics" @@ -24,18 +25,12 @@ import "core:terminal/ansi" @(private="file") stop_test_passed: libc.sig_atomic_t @(private="file") stop_test_alert: libc.sig_atomic_t -when ODIN_ARCH == .i386 && ODIN_OS == .Windows { - // Thread-local storage is problematic on Windows i386 - @(private="file") - local_test_index: libc.sig_atomic_t - @(private="file") - local_test_index_set: bool -} else { - @(private="file", thread_local) - local_test_index: libc.sig_atomic_t - @(private="file", thread_local) - local_test_index_set: bool -} + +@(private="file", thread_local) +local_test_index: libc.sig_atomic_t +@(private="file", thread_local) +local_test_index_set: bool + // Windows does not appear to have a SIGTRAP, so this is defined here, instead // of in the libc package, just so there's no confusion about it being diff --git a/core/testing/signal_handler_windows.odin b/core/testing/signal_handler_windows.odin index 74ebe2998..8843dde92 100644 --- a/core/testing/signal_handler_windows.odin +++ b/core/testing/signal_handler_windows.odin @@ -1,6 +1,211 @@ #+private +#+build windows package testing -__setup_signal_handler :: proc() {} +/* + (c) Copyright 2024 Feoramund . + Made available under Odin's license. + + List of contributors: + Feoramund: Total rewrite. + blob1807: Windows Win32 API rewrite. +*/ + +import "base:runtime" +import "base:intrinsics" + +import "core:os" +import "core:sync" +import "core:c/libc" +import "core:strconv" +import "core:terminal/ansi" + +import win32 "core:sys/windows" + + +@(private="file") stop_runner_flag: int + +@(private="file") stop_test_gate: sync.Mutex +@(private="file") stop_test_index: int +@(private="file") stop_test_signal: win32.DWORD +@(private="file") stop_test_passed: bool +@(private="file") stop_test_alert: int + + +when ODIN_ARCH == .i386 { + // Thread-local storage is problematic on Windows i386 + @(private="file") + local_test_index: int + @(private="file") + local_test_index_set: bool +} else { + @(private="file", thread_local) + local_test_index: int + @(private="file", thread_local) + local_test_index_set: bool +} + + +@(private="file") +stop_runner_callback :: proc "system" (ctrl_type: win32.DWORD) -> win32.BOOL { + if ctrl_type == win32.CTRL_C_EVENT { + prev := intrinsics.atomic_add(&stop_runner_flag, 1) + + // If the flag was already set (if this is the second signal sent for example), + // consider this a forced (not graceful) exit. + if prev > 0 { + os.exit(1) + } + // Say we've hanndled the signal. + return true + } + + // This will also get called for other events which we don't handle for. + // Instead we pass it on to the next handler. + return false +} + +@(private) +stop_test_callback :: proc "system" (info: ^win32.EXCEPTION_POINTERS) -> win32.LONG { + if !local_test_index_set { + // We're a thread created by a test thread. + // + // There's nothing we can do to inform the test runner about who + // signalled, so hopefully the test will handle their own sub-threads. + return win32.EXCEPTION_CONTINUE_SEARCH + } + + context = runtime.default_context() + code := info.ExceptionRecord.ExceptionCode + + if local_test_index == -1 { + // We're the test runner, and we ourselves have caught a signal from + // which there is no recovery. + // + // The most we can do now is make sure the user's cursor is visible, + // nuke the entire processs, and hope a useful core dump survives. + if !global_ansi_disabled { + show_cursor := ansi.CSI + ansi.DECTCEM_SHOW + os.write_string(os.stdout, show_cursor) + os.flush(os.stdout) + } + + expbuf: [8]byte + expstr := strconv.write_uint(expbuf[:], cast(u64)code, 16) + for &c in expbuf { + if 'a' <= c && c <= 'f' { + c -= 32 + } + } + + advisory_a := ` +The test runner's main thread has caught an unrecoverable error (exception 0x` + advisory_b := `) and will now forcibly terminate. +This is a dire bug and should be reported to the Odin developers. +` + os.write_string(os.stderr, advisory_a) + os.write_string(os.stderr, expstr) + os.write_string(os.stderr, advisory_b) + os.flush(os.stderr) + + win32.TerminateProcess(win32.GetCurrentProcess(), 1) + } + + if sync.mutex_guard(&stop_test_gate) { + intrinsics.atomic_store(&stop_test_index, local_test_index) + intrinsics.atomic_store(&stop_test_signal, code) + passed: bool + check_passing: { + if location := local_test_assertion_raised.location; location != {} { + for i in 0.. bool { + return intrinsics.atomic_load(&stop_runner_flag) == 1 +} + +@(private="file") +unlock_stop_test_gate :: proc(_: int, _: Stop_Reason, ok: bool) { + if ok { + sync.mutex_unlock(&stop_test_gate) + } +} + +@(deferred_out=unlock_stop_test_gate) +_should_stop_test :: proc() -> (test_index: int, reason: Stop_Reason, ok: bool) { + if intrinsics.atomic_load(&stop_test_alert) == 1 { + intrinsics.atomic_store(&stop_test_alert, 0) + + test_index = intrinsics.atomic_load(&stop_test_index) + if intrinsics.atomic_load(&stop_test_passed) { + reason = .Successful_Stop + } else { + switch intrinsics.atomic_load(&stop_test_signal) { + case win32.EXCEPTION_ILLEGAL_INSTRUCTION: reason = .Illegal_Instruction + case win32.EXCEPTION_ACCESS_VIOLATION: reason = .Segmentation_Fault + case win32.EXCEPTION_BREAKPOINT: reason = .Unhandled_Trap + case win32.EXCEPTION_SINGLE_STEP: reason = .Unhandled_Trap + + case win32.EXCEPTION_FLT_DENORMAL_OPERAND ..= win32.EXCEPTION_INT_OVERFLOW: + reason = .Arithmetic_Error + } + } + ok = true + } + + return +} -_test_thread_cancel :: proc "contextless" () {} diff --git a/core/thread/thread_other.odin b/core/thread/thread_other.odin index dde2a8e48..878119cbd 100644 --- a/core/thread/thread_other.odin +++ b/core/thread/thread_other.odin @@ -37,7 +37,7 @@ _destroy :: proc(thread: ^Thread) { unimplemented("core:thread procedure not supported on target") } -_terminate :: proc(using thread : ^Thread, exit_code: int) { +_terminate :: proc(thread : ^Thread, exit_code: int) { unimplemented("core:thread procedure not supported on target") } diff --git a/src/build_settings.cpp b/src/build_settings.cpp index e66d46713..6b8fefa59 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -366,9 +366,7 @@ enum OptInFeatureFlags : u64 { OptInFeatureFlag_IntegerDivisionByZero_AllBits, OptInFeatureFlag_ForceTypeAssert = 1u<<6, - - - + OptInFeatureFlag_UsingStmt = 1u<<7, }; u64 get_feature_flag_from_name(String const &name) { @@ -387,6 +385,9 @@ u64 get_feature_flag_from_name(String const &name) { if (name == "integer-division-by-zero:all-bits") { return OptInFeatureFlag_IntegerDivisionByZero_AllBits; } + if (name == "using-stmt") { + return OptInFeatureFlag_UsingStmt; + } if (name == "force-type-assert") { return OptInFeatureFlag_ForceTypeAssert; } diff --git a/src/check_stmt.cpp b/src/check_stmt.cpp index 2dc621a84..1f433df36 100644 --- a/src/check_stmt.cpp +++ b/src/check_stmt.cpp @@ -2945,10 +2945,12 @@ gb_internal void check_stmt_internal(CheckerContext *ctx, Ast *node, u32 flags) error(us->token, "Empty 'using' list"); return; } - if (check_vet_flags(node) & VetFlag_UsingStmt) { + + u64 feature_flags = check_feature_flags(ctx, node); + if ((feature_flags & OptInFeatureFlag_UsingStmt) == 0) { ERROR_BLOCK(); - error(node, "'using' as a statement is not allowed when '-vet' or '-vet-using' is applied"); - error_line("\t'using' is considered bad practice to use as a statement outside of immediate refactoring\n"); + error(node, "'using' has been disallowed as it is considered bad practice to use as a statement outside of immediate refactoring"); + error_line("\tIt you do require it for refactoring purposes or legacy code, it can be enabled on a per-file basis with '#+feature using-stmt'\n"); } for (Ast *expr : us->list) { diff --git a/src/check_type.cpp b/src/check_type.cpp index 41c5f48d1..b1d0045e9 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -1845,11 +1845,14 @@ gb_internal Type *check_get_params(CheckerContext *ctx, Scope *scope, Ast *_para Type *specialization = nullptr; bool is_using = (p->flags&FieldFlag_using) != 0; - if ((check_vet_flags(param) & VetFlag_UsingParam) && is_using) { - ERROR_BLOCK(); - error(param, "'using' on a procedure parameter is not allowed when '-vet' or '-vet-using-param' is applied"); - error_line("\t'using' is considered bad practice to use as a statement/procedure parameter outside of immediate refactoring\n"); + + u64 feature_flags = check_feature_flags(ctx, param); + + if (is_using && (feature_flags & OptInFeatureFlag_UsingStmt) == 0) { + ERROR_BLOCK(); + error(param, "'using' has been disallowed as it is considered bad practice to use as a statement/procedure parameter outside of immediate refactoring"); + error_line("\tIt you do require it for refactoring purposes or legacy code, it can be enabled on a per-file basis with '#+feature using-stmt'\n"); } if (type_expr == nullptr) { diff --git a/src/parser.cpp b/src/parser.cpp index c14055275..360537ab7 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -6504,6 +6504,8 @@ gb_internal u64 parse_feature_tag(Token token_for_pos, String s) { syntax_error(token_for_pos, "Invalid feature flag name: %.*s", LIT(p)); error_line("\tExpected one of the following\n"); error_line("\tdynamic-literals\n"); + error_line("\tglobal-context\n"); + error_line("\tusing-stmt\n"); error_line("\tinteger-division-by-zero:trap\n"); error_line("\tinteger-division-by-zero:zero\n"); error_line("\tinteger-division-by-zero:self\n"); diff --git a/vendor/fontstash/fontstash.odin b/vendor/fontstash/fontstash.odin index 772e379f0..33e7b68de 100644 --- a/vendor/fontstash/fontstash.odin +++ b/vendor/fontstash/fontstash.odin @@ -1,5 +1,5 @@ // An Odin-native source port of [[ Fontstash ; https://github.com/memononen/fontstash ]]. -#+vet !using-param +#+feature using-stmt package fontstash import "base:runtime"