Check comparison operands and walk instanceof chains as Node does

- The assert comparison methods read as function values (equal,
  strictEqual, deepEqual, partialDeepStrictEqual and their negations)
  throw Node's ERR_MISSING_ARGS TypeError, `The "actual" and "expected"
  arguments must be specified`, when called with fewer than two
  arguments, where `[assert.equal][0]()` passed.
- Untyped `instanceof Object` starts at the value's prototype, so
  Object.prototype itself, whose prototype is null, is not an Object.
- Untyped `instanceof Function` follows the whole prototype chain through
  objects and functions: an object whose prototype is a function, or a
  function whose prototype was set to such an object, is a Function.
This commit is contained in:
Chris Tate
2026-10-24 16:22:03 -05:00
parent 537dfbeb68
commit e00d3fd911
4 changed files with 102 additions and 10 deletions
+11
View File
@@ -3066,8 +3066,18 @@ static ScrDyn *scr_av_fail(ScrClosure *c, ScrDyn *const *args, size_t argc) {
return scr_av_done(msg);
}
/* The comparison methods take both operands: Node's ERR_MISSING_ARGS
* TypeError otherwise. False with it pending. */
static bool scr_av_operands(size_t argc) {
if (argc >= 2) return true;
static const char text[] = "The \"actual\" and \"expected\" arguments must be specified";
scr_throw_error_msg_code(SCR_ERR_TYPE, text, sizeof text - 1, "ERR_MISSING_ARGS");
return false;
}
/* equal / notEqual: Node's loose comparison, NaN equal to NaN. */
static ScrDyn *scr_av_loose(ScrDyn *const *args, size_t argc, bool negated) {
if (!scr_av_operands(argc)) return NULL;
const ScrDyn *a = scr_av_arg(args, argc, 0), *b = scr_av_arg(args, argc, 1);
bool nan = a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM && isnan(a->v.num) && isnan(b->v.num);
bool equal = nan || scr_dyn_abstract_eq(a, b);
@@ -3097,6 +3107,7 @@ static ScrDyn *scr_av_not_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
enum { SCR_AV_STRICT, SCR_AV_DEEP_STRICT, SCR_AV_DEEP, SCR_AV_PARTIAL };
static ScrDyn *scr_av_compare(ScrDyn *const *args, size_t argc, int form, bool negated) {
if (!scr_av_operands(argc)) return NULL;
ScrDyn *a = (ScrDyn *)scr_av_arg(args, argc, 0), *b = (ScrDyn *)scr_av_arg(args, argc, 1);
ScrStr *msg;
bool has;
+54 -10
View File
@@ -4684,12 +4684,18 @@ static bool scr_dyn_instanceof_object(const ScrDyn *value) {
default:
break;
}
/* Object.prototype is on the chain from the value's own prototype on:
* Object.prototype itself has a null prototype, so it is not an Object. */
ScrDyn *current = scr_dyn_retain((ScrDyn *)value);
for (;;) {
if (current == scr_builtin_object_prototype) {
for (bool first = true;; first = false) {
if (!first && current == scr_builtin_object_prototype) {
scr_dyn_release(current);
return true;
}
if (current == scr_builtin_object_prototype) {
scr_dyn_release(current);
return false;
}
ScrDyn *next = NULL;
if (current->kind == SCR_DYN_FUNC) {
/* A function's prototype is Function.prototype, whose own is
@@ -4719,15 +4725,53 @@ static bool scr_dyn_instanceof_object(const ScrDyn *value) {
}
}
/* `value instanceof Function`: every function, unless the program set its
* prototype to null or to a non-function. */
/* `value instanceof Function`: whether a function is on the value's
* prototype chain. A function's own prototype is Function.prototype unless
* the program set it, so the walk ends true at the first function that
* kept its prototype; it follows set prototypes through functions and
* objects alike (`Object.create(function () {})` is a Function instance),
* and ends false at null or at a value without a chain the runtime walks.
* False with the exception pending when a prototype read throws. */
static bool scr_dyn_instanceof_function(const ScrDyn *value) {
if (value->kind != SCR_DYN_FUNC) return false;
if (!scr_closure_identity(value->v.fn.clo)->props) return true;
ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value);
bool result = table->prototype ? scr_dyn_instanceof_function(table->prototype) : !table->null_proto;
scr_dyn_release(table);
return result;
ScrDyn *current = NULL;
if (value->kind == SCR_DYN_FUNC) {
if (!scr_closure_identity(value->v.fn.clo)->props) return true;
ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value);
current = table->prototype ? scr_dyn_retain(table->prototype) : NULL;
bool unset = !table->prototype && !table->null_proto;
scr_dyn_release(table);
if (unset) return true;
} else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR) {
current = scr_dyn_get_prototype((ScrDyn *)value);
if (!current) return false; /* the read threw: pending */
}
while (current && current->kind != SCR_DYN_NULL) {
ScrDyn *next = NULL;
if (current->kind == SCR_DYN_FUNC) {
if (!scr_closure_identity(current->v.fn.clo)->props) {
scr_dyn_release(current);
return true;
}
ScrDyn *table = scr_dyn_fn_properties(current);
bool unset = !table->prototype && !table->null_proto;
next = table->prototype ? scr_dyn_retain(table->prototype) : NULL;
scr_dyn_release(table);
if (unset) {
scr_dyn_release(current);
return true;
}
} else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR) {
next = scr_dyn_get_prototype(current);
if (!next) {
scr_dyn_release(current);
return false; /* the read threw: pending */
}
}
scr_dyn_release(current);
current = next;
}
if (current) scr_dyn_release(current);
return false;
}
bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) {
@@ -53,3 +53,25 @@ function describe(value) {
return value instanceof Function ? 'function' : 'object';
}
console.log([nullProto, {}, describe, 0].map(describe).join(', '));
// Prototype chains through functions and objects, and Object.prototype
// itself, whose own prototype is null.
const base = function base() {};
const fromFunction = {};
Object.setPrototypeOf(fromFunction, base);
const throughObject = {};
Object.setPrototypeOf(throughObject, fromFunction);
const nullRooted = function nullRooted() {};
Object.setPrototypeOf(nullRooted, null);
const reparented = function reparented() {};
Object.setPrototypeOf(reparented, fromFunction);
const chains = {
objectPrototype: Object.getPrototypeOf({}),
fromFunction,
throughObject,
nullRooted,
reparented,
};
for (const [label, value] of Object.entries(chains)) {
console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`);
}
@@ -28,3 +28,18 @@ const checks = [assert.ok, assert.fail, assert.strictEqual];
checks.forEach((check, i) => console.log(i, typeof check, check === checks[i], check.name));
[1, 'x', true].forEach((value) => assert.ok(value));
console.log('done');
// The comparison methods need both operands, as Node's ERR_MISSING_ARGS
// TypeError says, also through function values.
const comparisons = [assert.equal, assert.notEqual, assert.strictEqual, assert.notStrictEqual,
assert.deepEqual, assert.notDeepEqual, assert.deepStrictEqual, assert.notDeepStrictEqual];
for (const compare of comparisons) {
for (const args of [[], [1]]) {
try {
compare(...args);
console.log(compare.name, args.length, 'passed');
} catch (e) {
console.log(compare.name, args.length, e.name, e.code, e.message);
}
}
}