diff --git a/AppKit/CPScroller.j b/AppKit/CPScroller.j index b3c97893d..a9cd1de18 100644 --- a/AppKit/CPScroller.j +++ b/AppKit/CPScroller.j @@ -23,12 +23,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "../Foundation/Foundation.h" @import "CPAnimation.j" @import "CPControl.j" -@import "CPWindow_Constants.j" @import "CPViewAnimation.j" +@import "CPWindow_Constants.j" @global CPApp @@ -259,7 +258,7 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark"); */ - (void)setKnobProportion:(float)aProportion { - if (!_IS_NUMERIC(aProportion)) + if (!CPIsNumeric(aProportion)) [CPException raise:CPInvalidArgumentException reason:"aProportion must be numeric, was: "+aProportion]; _knobProportion = MIN(1.0, MAX(0.0001, aProportion)); diff --git a/AppKit/CPSplitView.j b/AppKit/CPSplitView.j index 1b4d8c08e..67edc2864 100644 --- a/AppKit/CPSplitView.j +++ b/AppKit/CPSplitView.j @@ -23,13 +23,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "../Foundation/Foundation.h" - @import "CPButtonBar.j" -@import "CPImage.j" -@import "CPView.j" @import "CPCursor.j" +@import "CPImage.j" @import "CPTrackingArea.j" +@import "CPView.j" @class CPUserDefaults @global CPApp @@ -1171,7 +1169,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle // Silently ignore bad positions which could result from odd delegate responses. We don't want these // bad results to go into the system and cause havoc with frame sizes as the split view tries to resize // its subviews. - if (_IS_NUMERIC(proposedPosition)) + if (CPIsNumeric(proposedPosition)) position = proposedPosition; var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex], @@ -1181,10 +1179,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle proposedActualMin = [self _sendDelegateSplitViewConstrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex], proposedActualMax = [self _sendDelegateSplitViewConstrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex]; - if (_IS_NUMERIC(proposedActualMin)) + if (CPIsNumeric(proposedActualMin)) actualMin = proposedActualMin; - if (_IS_NUMERIC(proposedActualMax)) + if (CPIsNumeric(proposedActualMax)) actualMax = proposedActualMax; var viewA = _arrangedSubviews[dividerIndex], diff --git a/AppKit/CPStackView.j b/AppKit/CPStackView.j index dd44f73c4..c05d776b9 100644 --- a/AppKit/CPStackView.j +++ b/AppKit/CPStackView.j @@ -20,8 +20,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "../Foundation/Foundation.h" - @import "CPView.j" // Gravity Areas diff --git a/Foundation/CPIndexSet.j b/Foundation/CPIndexSet.j index e478eaade..e655a2445 100644 --- a/Foundation/CPIndexSet.j +++ b/Foundation/CPIndexSet.j @@ -20,8 +20,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "Foundation.h" - @import "CPArray.j" @import "CPObject.j" @import "CPRange.j" @@ -79,7 +77,7 @@ */ - (id)initWithIndex:(CPInteger)anIndex { - if (!_IS_NUMERIC(anIndex)) + if (!CPIsNumeric(anIndex)) [CPException raise:CPInvalidArgumentException reason:"Invalid index"]; diff --git a/Foundation/Foundation.h b/Foundation/Foundation.h deleted file mode 100644 index b5666e138..000000000 --- a/Foundation/Foundation.h +++ /dev/null @@ -1,2 +0,0 @@ -// By Christian C. Salvadó, http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric/1830844#1830844 -#define _IS_NUMERIC(n) (!isNaN(parseFloat(n)) && isFinite(n)) diff --git a/Foundation/Foundation.j b/Foundation/Foundation.j index e2d752b17..a78ad4c0f 100755 --- a/Foundation/Foundation.j +++ b/Foundation/Foundation.j @@ -21,6 +21,7 @@ */ @import "_CGGeometry.j" +@import "_CPFoundationUtilities.j" @import "CPArray.j" @import "CPBundle.j" @import "CPByteCountFormatter.j" diff --git a/Foundation/_CPFoundationUtilities.j b/Foundation/_CPFoundationUtilities.j new file mode 100644 index 000000000..0d44f09a9 --- /dev/null +++ b/Foundation/_CPFoundationUtilities.j @@ -0,0 +1,88 @@ +/* + * _CPFoundationUtilities.j + * Foundation + * + * Created by David Richardson. + * Copyright 2026, Cappuccino Project. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +//#define _IS_NUMERIC(n) (!isNaN(parseFloat(n)) && isFinite(n)) + +/* + Objective-J is a strict superset of JavaScript and compiles down to a shared global runtime scope. + This file (_CPFoundationUtilities.j) is the canonical place for otherwise "homeless" low-level + utilities, stateless helpers, and former C-style preprocessor macros that do not belong to a + specific class but are required across the framework. + + While modern JavaScript ecosystems (e.g., ES6 modules, bundlers) treat the global scope as something + to be strictly avoided, Cappuccino's architecture predates these paradigms. It relies entirely on + global scope sharing for its runtime and toll-free bridging with native JavaScript, much like C and + Objective-C. Therefore, injecting `CP`-prefixed functions into the global scope is the intended design + pattern here, not an anti-pattern or "pollution." + + ---------------------------------------------------------------------------- + New compiler does not provide a pre-processor. + The macro is expanded here to a concrete function. + + The alternative is inlining at call site. + On a cold call, this provides a very minor performance advantage. + Conversely, it provides the Javascript engine fewer opportunities to optimize, + which is only done when a call site is invoked. + Additionally, it depends on individual maintainers to correctly implement the call every time. + + The macro is expanded here precisely to maintain identical semantics. + + Every current, popular browser engine optimizes a small, hot, monomorphic function like a numeric check almost immediately: + + V8 (Chrome, Edge, Opera, Brave, Node) — tiered JIT (Ignition → Sparkplug → Maglev → TurboFan). + A function called this often gets promoted within tens of calls. + SpiderMonkey (Firefox) — Baseline Interpreter → Baseline JIT → Ion. Same pattern. + JavaScriptCore (Safari, all iOS browsers, since iOS forces WebKit) — LLInt → Baseline → DFG → FTL. + + All three engines specialize aggressively on exactly this shape of code: a tiny, pure, argument-type-stable function with no side effects. It is close to the ideal case for JIT optimization — the compiler will likely inline the call at the machine-code level, which is the same outcome as hand-inlining the expression, achieved automatically. + + There is no browser in current popular use — desktop or mobile — where this function call would remain a meaningful cost. + Additionally, modern hardware and Javascript engines are so much faster than in 2008, when Cappuccino was conceived, + that even a cold execution of this function is trivial. + The performance objection which originally required in-lining via a macro does not exist for current targets. + + Javascript, Objective-J, C, and Objective-C all lack native namespacing. + 'CP' is the canonical namespace prefix used throughout Cappuccino to address potential collisions. + It is reserved by convention. + */ + +/* + Checks if a value is a valid, finite number. + + This implements the legacy `_IS_NUMERIC` behavior exactly. It returns true for + numbers and strings that can be successfully parsed into a finite number (e.g., 42, "3.14"), + and false for NaN, Infinity, null, and purely non-numeric strings. + + This specific logic (parseFloat + global isFinite) is deliberately preserved to prevent + regressions in code that historically relied on its lenient string parsing, rather than + using the stricter modern ES6 `Number.isFinite()`. + + TODO: Modernize this check to use ES6 `Number.isFinite()`. This is currently deferred + to maintain strict semantic continuity during the Go/Lisette toolchain migration and + requires a full audit of all call sites to ensure string coercion is no longer expected. + + @param n The value to evaluate. + @return {Boolean} YES if the value is numeric, NO otherwise. + */ +function CPIsNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +}