Shared formatter in sx_types.ml. Small integer-valued floats still print
as plain ints; floats outside safe-int range (|n| >= 1e16) now print as
%.17g (full precision) instead of silently wrapping to negative or 0.
Non-integer values keep %g 6-digit behavior — no existing SX tests regress.
Unblocks Number.MAX_VALUE / Math.pow(2,N) style tests in js-on-sx where
iterative float loops were collapsing to 0 at ~2^63.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Number.prototype.constructor === Number etc. Four dict-set! lines add
the backlink after each constructor dict is defined.
new String().constructor === String now returns true. Array literals
don't yet link to Array.prototype so [].constructor === Array is still
false — that would need a boxing refactor.
Unit 521/522, slice 148/148 unchanged.
Number 76/100 → 77/100 (+1). String variance-heavy under CPU load.
Wire up the `ask` and `answer` commands end-to-end:
- tokenizer.sx: register `ask` and `answer` as hs-keywords.
- parser.sx: cmd-kw? gains both; parse-cmd dispatches to new
parse-ask-cmd (emits `(ask MSG)`) and parse-answer-cmd, which
reads `answer MSG [with YES or NO]`. The with/or pair reads
yes/no via parse-atom — parse-expr would collapse
`"Yes" or "No"` into `(or "Yes" "No")` before match-kw "or"
could fire. The no-`with` form emits `(answer-alert MSG)`.
- compiler.sx: three new cond branches (ask, answer, answer-alert)
compile to a let that binds __hs-a, sets `the-result` and `it`,
and returns the value — so `then put it into ...` works.
- runtime.sx: hs-ask / hs-answer / hs-answer-alert call
window.prompt / confirm / alert via host-call + host-global.
- tests/hs-run-filtered.js: test-name-keyed globalThis.{alert,
confirm,prompt}; __currentHsTestName is updated before each
test. Host-set! for innerHTML/textContent now coerces JS
null → "null" (browser behaviour) so `prompt → null` →
`put it into #out` renders literal text "null", which the
fourth test depends on.
Suite hs-upstream-askAnswer: 1/5 -> 5/5.
Smoke 0-195: 166/195 -> 170/195.
Three parser additions so scripts like `(on click (log me) (trigger foo))`
parse into a single feature with both commands in its body:
1. parse-feat: new cond branch for `paren-open` — advance, recurse
parse-feat, consume `paren-close`. Allows a feature like `(on click
...)` to be grouped in parens.
2. parse-cmd: two new cond branches — on `paren-close` return nil (so
cl-collect terminates at an outer group close), and on `paren-open`
advance / recurse / close. Allows single parenthesized commands like
`(log me)`.
3. cl-collect: previously only recursed when the next token was a
recognised command keyword (`cmd-kw?`), so after `(log me)` the
sibling `(trigger foo)` would end the feature body and re-surface as
a top-level feature. Extended the recursion predicate to also fire
when the next token is `paren-open`.
Suite hs-upstream-core/parser: 9/14 -> 10/14.
Smoke 0-195: 165/195 -> 166/195.
SX's (str 1e-7) gives "1e-07" but JS spec is "1e-7" — no padding, no
leading zeros in the exponent (sign stays). We stepped through:
mant "e" expraw → mant "e" (sign (strip-zeros body))
Added four small helpers: js-normalize-num-str, js-split-sign,
js-strip-leading-zeros, js-strip-zeros-loop. All pure string walkers.
Unit 521/522, slice 148/148 unchanged.
String 40 → 42, Number 75 → 76 (+3 total).
Fixes S9.8.1_A9_T1, fromCharCode/S9.7_A3.1_T1..T2 family.
Tests expected Function.prototype.isPrototypeOf(Number/String/…) ===
true because every built-in ctor inherits from Function.prototype.
Our model doesn't link Number.__proto__ anywhere, so the default
Object.isPrototypeOf walked an empty chain and returned false.
Fix: post-definition dict-set! adds an explicit isPrototypeOf override
on js-function-global.prototype that returns (js-function? x) — which
accepts lambdas, functions, components, and __callable__ dicts. Good
enough to satisfy the spec for every case that isn't a bespoke proto
chain.
Unit 521/522, slice 148/148 unchanged.
Wide scoreboard: 156/300 → 159/300 (+3, Number/S15.7.3_A7 and the
three S15.5.3_A2 / S15.6.3_A2 / S15.9.3_A2 twins).
Every built-in JS function on Math/Number/Array/Object had .name === ""
because js-invoke-function-method/js-get-prop returned bare "" for the
"name" slot. That breaks tests like Math.abs.name === "abs" and
Array.isArray.name === "isArray".
Fix: extract the SX symbol name from (inspect fn) which prints
<js-math-abs(x)>, then unmap through a small string table that maps
js-math-abs → "abs", js-array-is-array → "isArray" etc. Also strips
the angle-bracket marker and stops at ( or space.
Non-mapped lambdas (user fns) fall through to the raw "js-foo" form
rather than "", which is slightly worse but only hit in debug prints.
Unit 521/522, slice 148/148 unchanged.
Scoreboard: Math 40/100 → 43/100 (+3); Number 74 → 75 (+1).
Sample: Math/abs/name.js, Math/floor/name.js, Math/max/name.js,
Number/isNaN/name.js — all flipped. length.js tests still fail for
trig because the underlying fn isn't implemented.
js-number-to-string did (str n), which gives OCaml-native "inf"/"-inf"/
"nan"/"-nan" strings. JS spec requires "Infinity"/"-Infinity"/"NaN".
Fix: cond-check js-number-is-nan, and =infinity-value first, fall
through to (str n) for finite.
Unit 521/522, slice 148/148 unchanged.
String scoreboard: 34/100 → 38/100 (+4, S15.5.1.1_A1_T11/T12 family —
String(1/0)/String(-1/0)/String(0/0)).
Previously a JS function body with no return fell through the call/cc
begin as nil, making `(function(){}())` return null and typeof → object.
Spec: falls-off-end gives undefined.
Wrap the call/cc in (let ((__r__ ...)) (if (= __r__ nil) :js-undefined __r__)).
Downside: explicit `return null` also returns nil, but so does (pick
your last expression evaluating to null). For 99% of cases it's
fall-off-end and the fix is correct. Code that genuinely needs
distinguishable null would need separate nil/undef handling in the
evaluator.
Unit 521/522, slice 148/148 unchanged.
Number 73/100 → 74/100 (+1), String 33/100 → 34/100 (+1).
Fixes S15.5.1.1_A1_T1 family (String(function(){}()) should be "undefined").
ES spec: ToNumber("0x0") == 0, ToNumber("0xFF") == 255. Our parser
returned NaN for anything with a 0x/0X prefix, failing S9.3.1_A16..A32
(every hex-literal assertion test case).
Added:
- js-hex-prefix? — is this an 0x/0X prefix?
- js-is-hex-body? — all remaining chars are [0-9a-fA-F]?
- js-parse-hex — walk chars, accumulate in base 16, NaN on bad char
- js-hex-digit-value — char → 0..15 (or -1)
js-is-numeric-string? short-circuits to the hex-body check on 0x*
prefix; js-num-from-string dispatches to js-parse-hex on same.
Unit 521/522, slice 148/148 unchanged.
Number scoreboard: 58/100 → 73/100 (+15).
Sample flipped: S9.3.1_A16 (0x0/0X0), A17..A31 (0x0..0xF and 0x10..0x1F
range coverage). Many of the remaining 22 fails are (new Number()).x
style prototype-chain introspection and MAX_VALUE precision.
js-get-ctor-proto used to always synthesise a per-ctor-id empty dict in
__js_proto_table__, ignoring the :prototype slot that every built-in
constructor dict (Number, String, Array, Boolean, Object, Function)
already carries. So `new Number()` got `{__proto__: {}}` instead of
`{__proto__: Number.prototype}`, breaking every prototype-chain-method
lookup:
(new Number()).toLocaleString // undefined → function
(new Number()).toLocaleString === Number.prototype.toLocaleString
Fix: when the receiver is a dict with a "prototype" key, return that
directly; otherwise fall through to the existing proto-table path.
Declared classes from JS still go through the table because they emit
js-set-ctor-proto! at definition.
Unit 521/522, slice 148/148 unchanged.
Number scoreboard: 53/100 → 58/100 (+5). Sample: S15.7.5_A1_T03..T07
(toLocaleString/toString/toFixed/toExponential prototype identity).
Six post-definition dict-set! calls add .length=1 and .name="<Ctor>"
to the global constructor dicts. Per spec Number/String/Array/Boolean/
Object all have length 1 (they take one arg).
Unit 521/522, slice 148/148 unchanged.
Number scoreboard: 52/100 → 53/100 (+1, S15.7.3_A8: Number.length==1).