mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Walk for-in and Object.keys keys natively for unknown values
This commit is contained in:
@@ -1244,7 +1244,8 @@ function liveRecordOps(
|
||||
`}`,
|
||||
``,
|
||||
`@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`,
|
||||
`@${ops} = internal constant { ${host.sizeType}, ptr, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get} }`,
|
||||
// Mutable: the runtime fills the trailing key-array slot on first use.
|
||||
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1027,6 +1027,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// (catchable SyntaxError) — all may-throw generics over the checked-dynamic tree.
|
||||
"dyn.objKeys": "scr_dyn_obj_keys",
|
||||
"dyn.forInKeys": "scr_dyn_for_in_keys",
|
||||
"dyn.forInKeyStrings": "scr_dyn_for_in_key_strs",
|
||||
"dyn.forInStable": "scr_dyn_for_in_stable",
|
||||
"dyn.objKeyStrings": "scr_dyn_obj_key_strs",
|
||||
"dyn.hasOwn": "scr_dyn_has_own",
|
||||
"dyn.propertyIsEnumerable": "scr_dyn_property_is_enumerable",
|
||||
"dyn.assign": "scr_dyn_assign",
|
||||
|
||||
@@ -593,7 +593,8 @@ export class ClassDynamicDispatch {
|
||||
private readonly prototypeIdentityClasses = new Set<string>();
|
||||
private readonly instancePrototypes = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
private readonly checkedCasts = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
private enumeration: IrFunction | null = null;
|
||||
/** The for-in key helpers, by libCall (dyn and native string[] keys). */
|
||||
private readonly enumerations = new Map<"dyn.forInKeys" | "dyn.forInKeyStrings", IrFunction>();
|
||||
private readonly enumerationClasses = new Set<string>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
@@ -690,6 +691,7 @@ export class ClassDynamicDispatch {
|
||||
expr.fn === "dyn.toString" ||
|
||||
expr.fn === "dyn.stringConstructor" ||
|
||||
expr.fn === "dyn.forInKeys" ||
|
||||
expr.fn === "dyn.forInKeyStrings" ||
|
||||
expr.fn === "dyn.hasKeyComputed" ||
|
||||
expr.fn === "dyn.hasOwnComputed" ||
|
||||
expr.fn === "dyn.propertyIsEnumerableComputed" ||
|
||||
@@ -1353,27 +1355,35 @@ export class ClassDynamicDispatch {
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (expr.kind === "libCall" && expr.fn === "dyn.forInKeys") {
|
||||
if (!this.enumeration) {
|
||||
if (
|
||||
expr.kind === "libCall" &&
|
||||
(expr.fn === "dyn.forInKeys" || expr.fn === "dyn.forInKeyStrings")
|
||||
) {
|
||||
let enumeration = this.enumerations.get(expr.fn);
|
||||
if (!enumeration) {
|
||||
const loc = expr.loc;
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
this.enumeration = {
|
||||
name: "%dyn.class.forInKeys",
|
||||
enumeration = {
|
||||
name:
|
||||
expr.fn === "dyn.forInKeys"
|
||||
? "%dyn.class.forInKeys"
|
||||
: "%dyn.class.forInKeyStrings",
|
||||
params: [{ localId: "p.0", name: "value", type: DYN }],
|
||||
returnType: DYN,
|
||||
returnType: expr.type,
|
||||
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: { ...expr, args: [value] }, loc }],
|
||||
loc,
|
||||
};
|
||||
this.generated.add(this.enumeration);
|
||||
lowerer.liftedFns.push(this.enumeration);
|
||||
this.enumerations.set(expr.fn, enumeration);
|
||||
this.generated.add(enumeration);
|
||||
lowerer.liftedFns.push(enumeration);
|
||||
changed = true;
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: this.enumeration.name,
|
||||
callee: enumeration.name,
|
||||
args: expr.args,
|
||||
type: DYN,
|
||||
type: expr.type,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
@@ -2701,20 +2711,21 @@ export class ClassDynamicDispatch {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (this.enumeration)
|
||||
for (const [fn, enumeration] of this.enumerations)
|
||||
for (const plan of this.propertyReceivers(lowerer, false, false)) {
|
||||
if (!plan.info.def.tracksOwnFields || this.enumerationClasses.has(plan.key)) continue;
|
||||
this.enumerationClasses.add(plan.key);
|
||||
const loc = this.enumeration.loc;
|
||||
const classKey = `${fn}:${plan.key}`;
|
||||
if (!plan.info.def.tracksOwnFields || this.enumerationClasses.has(classKey)) continue;
|
||||
this.enumerationClasses.add(classKey);
|
||||
const loc = enumeration.loc;
|
||||
const { condition } = this.propertyReceiver(plan, loc);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, varRef("p.0", DYN, loc), loc);
|
||||
this.enumeration.body.unshift({
|
||||
enumeration.body.unshift({
|
||||
kind: "if",
|
||||
cond: condition,
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "libCall", fn: "dyn.forInKeys", args: [bag], type: DYN, loc },
|
||||
value: { kind: "libCall", fn, args: [bag], type: enumeration.returnType, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -7388,6 +7388,21 @@ function lowerObjectStaticCall(
|
||||
: "dyn.objEntries";
|
||||
let v = lowerer.lowerExpr(argNode);
|
||||
if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
|
||||
// In TypeScript the keys come back as a native string[]: the checker
|
||||
// keeps every later write to the array a string. JavaScript may store
|
||||
// anything into it, so it keeps the checked-dynamic array.
|
||||
const keysT =
|
||||
member === "keys" && !isJsSourceFile(call.getSourceFile())
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(call))
|
||||
: null;
|
||||
if (keysT?.kind === "array" && keysT.elem.kind === "string")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.objKeyStrings",
|
||||
args: [v],
|
||||
type: keysT,
|
||||
loc: locOf(call),
|
||||
};
|
||||
return { kind: "libCall", fn, args: [v], type: DYN, loc: locOf(call) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9558,17 +9558,35 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt {
|
||||
const recv = lowerer.declareHiddenLocal("%indyn", DYN);
|
||||
const ref: IrExpr = { kind: "varRef", localId: recv.id, type: DYN, loc };
|
||||
const keys: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "libCall", fn: "dyn.forInKeys", args: [ref], type: DYN, loc },
|
||||
kind: "libCall",
|
||||
fn: "dyn.forInKeyStrings",
|
||||
args: [ref],
|
||||
type: arrayOf(STRING),
|
||||
loc,
|
||||
};
|
||||
const loop = lowerForInOverKeys(lowerer, stmt, keys, labels, (key) =>
|
||||
classForInHasKey(lowerer, ref, key, loc),
|
||||
);
|
||||
// A stable snapshot (a typed record's fields) needs no live presence
|
||||
// test per visit.
|
||||
const stable = lowerer.declareHiddenLocal("%instable", BOOL);
|
||||
const loop = lowerForInOverKeys(lowerer, stmt, keys, labels, (key) => ({
|
||||
kind: "ternary",
|
||||
cond: { kind: "varRef", localId: stable.id, type: BOOL, loc },
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: classForInHasKey(lowerer, ref, key, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
}));
|
||||
return {
|
||||
kind: "block",
|
||||
body: [{ kind: "varDecl", localId: recv.id, init: receiver, loc }, loop],
|
||||
body: [
|
||||
{ kind: "varDecl", localId: recv.id, init: receiver, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: stable.id,
|
||||
init: { kind: "libCall", fn: "dyn.forInStable", args: [ref], type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
loop,
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,6 +397,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// the dyn Object walks throw on null/undefined receivers
|
||||
"dyn.objKeys",
|
||||
"dyn.forInKeys",
|
||||
"dyn.forInKeyStrings",
|
||||
"dyn.objKeyStrings",
|
||||
"dyn.hasOwn",
|
||||
"dyn.propertyIsEnumerable",
|
||||
"dyn.assign",
|
||||
|
||||
@@ -2668,6 +2668,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* ("Cannot convert undefined or null to object"). */
|
||||
"dyn.objKeys": { argTypes: [DYN], result: DYN },
|
||||
"dyn.forInKeys": { argTypes: [DYN], result: DYN },
|
||||
"dyn.forInKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) },
|
||||
"dyn.forInStable": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.objKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) },
|
||||
"dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL },
|
||||
"dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL },
|
||||
"dyn.assign": { argTypes: [DYN, DYN], result: DYN },
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function emit(source: string, file = "main.ts"): Promise<string> {
|
||||
const outDir = await mkdtemp(join(tmpdir(), "scriptc-forin-"));
|
||||
directories.push(outDir);
|
||||
const entry = join(outDir, file);
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(outDir, "program.ll");
|
||||
const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
return readFile(outPath, "utf8");
|
||||
}
|
||||
|
||||
test("for-in over a checked-dynamic value walks native string keys", async () => {
|
||||
const llvm = await emit(`
|
||||
type Row = { id: number; name: string };
|
||||
function count(o: unknown): number {
|
||||
let n = 0;
|
||||
for (const k in o as Record<string, unknown>) n += k.length;
|
||||
return n;
|
||||
}
|
||||
const row: Row = { id: 1, name: "a" };
|
||||
console.log(count(row), count(JSON.parse('{"b":1}')));
|
||||
`);
|
||||
expect(llvm).toContain("call ptr @scr_dyn_for_in_key_strs(");
|
||||
expect(llvm).toContain("call zeroext i1 @scr_dyn_for_in_stable(");
|
||||
expect(llvm).not.toContain("@scr_dyn_for_in_keys(");
|
||||
// Fully required record shapes are marked fixed for the stable walk.
|
||||
expect(llvm).toMatch(
|
||||
/_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{[^}]*i8 1, ptr null \}/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Object.keys of an unknown value is a native string[] in TypeScript only", async () => {
|
||||
const source = `
|
||||
function keysOf(o) {
|
||||
return Object.keys(o);
|
||||
}
|
||||
console.log(keysOf(JSON.parse('{"a":1}')).length);
|
||||
`;
|
||||
const ts = await emit(
|
||||
source.replace("(o)", "(o: unknown)").replace("keys(o)", "keys(o as object)"),
|
||||
);
|
||||
expect(ts).toContain("call ptr @scr_dyn_obj_key_strs(");
|
||||
const js = await emit(source, "main.js");
|
||||
expect(js).not.toContain("@scr_dyn_obj_key_strs(");
|
||||
expect(js).toContain("call ptr @scr_dyn_obj_keys(");
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5175-forin-typed-as-unknown.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5176-object-keys-typed-as-unknown.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5177-forin-dynamic-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5178-forin-keys-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5179-forin-typed-shapes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -8577,6 +8577,120 @@ ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Whether for-in's key snapshot of `v` stays exact through the walk, so
|
||||
* visits need no live presence test: a typed record whose fields are all
|
||||
* required cannot lose a key. */
|
||||
bool scr_dyn_for_in_stable(const ScrDyn *v) {
|
||||
if (v->kind != SCR_DYN_TYPED_REF) return false;
|
||||
const ScrDynTypedRecordOps *record = scr_typed_ref_record_ops(v);
|
||||
return record != NULL && record->fixed;
|
||||
}
|
||||
|
||||
/* The record shape's key array for walks that only read it: built once,
|
||||
* immortal, and shared by every thread through the ops' atomic slot. */
|
||||
static ScrArr *scr_record_shared_keys(const ScrDynTypedRecordOps *record) {
|
||||
ScrArr **slot = &((ScrDynTypedRecordOps *)record)->keys;
|
||||
ScrArr *keys = __atomic_load_n(slot, __ATOMIC_ACQUIRE);
|
||||
if (keys) return keys;
|
||||
ScrArr *built = scr_arr_new(SCR_ELEM_STR, record->count);
|
||||
for (size_t i = 0; i < record->count; i++)
|
||||
scr_arr_push_ref(built, scr_str_retain(record->names[i]));
|
||||
built->rc = SIZE_MAX;
|
||||
ScrArr *expected = NULL;
|
||||
if (__atomic_compare_exchange_n(slot, &expected, built, false, __ATOMIC_ACQ_REL,
|
||||
__ATOMIC_ACQUIRE)) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_arr_live_forget(); /* immortal: outside the exit-time RC audit */
|
||||
#endif
|
||||
return built;
|
||||
}
|
||||
built->rc = 1;
|
||||
scr_arr_release(built);
|
||||
return expected;
|
||||
}
|
||||
|
||||
/* A typed record's present field names, or a plain object's enumerable
|
||||
* own keys (integer-like keys ascending, then insertion order), as a
|
||||
* native string[] (+1); NULL for every other value. `shared`: the caller
|
||||
* only reads the array, so a record shape's cached array may serve. */
|
||||
static ScrArr *scr_dyn_own_key_strs(const ScrDyn *v, bool shared) {
|
||||
if (v->kind == SCR_DYN_TYPED_REF) {
|
||||
const ScrDynTypedRecordOps *record = scr_typed_ref_record_ops(v);
|
||||
if (!record) return NULL;
|
||||
void *rec = v->v.typed_ref.ptr;
|
||||
bool all = true;
|
||||
for (size_t i = 0; i < record->count && all; i++) all = record->has(rec, i);
|
||||
if (shared && all) return scr_record_shared_keys(record);
|
||||
ScrArr *out = scr_arr_new(SCR_ELEM_STR, record->count);
|
||||
for (size_t i = 0; i < record->count; i++)
|
||||
if (record->has(rec, i)) scr_arr_push_ref(out, scr_str_retain(record->names[i]));
|
||||
return out;
|
||||
}
|
||||
if (v->kind != SCR_DYN_OBJ) return NULL;
|
||||
size_t n = v->v.obj.len;
|
||||
ScrArr *out = scr_arr_new(SCR_ELEM_STR, n);
|
||||
ScrKeyIndex *indices = NULL;
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const ScrDynEntry *entry = &v->v.obj.entries[i];
|
||||
uint32_t index;
|
||||
if (!entry->enumerable || !scr_key_array_index(entry->key, entry->key_len, &index)) continue;
|
||||
if (!indices) {
|
||||
indices = malloc(n * sizeof *indices);
|
||||
if (!indices) scr_json_oom();
|
||||
}
|
||||
indices[count++] = (ScrKeyIndex){index, i};
|
||||
}
|
||||
if (count) {
|
||||
scr_key_index_sort(indices, count);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const ScrDynEntry *entry = &v->v.obj.entries[indices[i].entry];
|
||||
scr_arr_push_ref(out, scr_str_new(entry->key, entry->key_len));
|
||||
}
|
||||
}
|
||||
free(indices);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const ScrDynEntry *entry = &v->v.obj.entries[i];
|
||||
uint32_t index;
|
||||
if (entry->enumerable && !scr_key_array_index(entry->key, entry->key_len, &index))
|
||||
scr_arr_push_ref(out, scr_str_new(entry->key, entry->key_len));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A dyn string array's strings as a native string[] (+1), releasing it. */
|
||||
static ScrArr *scr_dyn_key_strs_from(ScrDyn *keys) {
|
||||
if (!keys) return NULL;
|
||||
ScrArr *out = scr_arr_new(SCR_ELEM_STR, keys->v.arr.len);
|
||||
for (size_t i = 0; i < keys->v.arr.len; i++)
|
||||
scr_arr_push_ref(out, scr_str_retain(keys->v.arr.items[i]->v.str));
|
||||
scr_dyn_release(keys);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* for-in's key snapshot as a native string[] (+1): the typed record's
|
||||
* field names, or a plain object's enumerable own keys (integer-like keys
|
||||
* ascending, then insertion order) when its prototype contributes none.
|
||||
* Every other value takes the dyn walk and converts its result. */
|
||||
ScrArr *scr_dyn_for_in_key_strs(const ScrDyn *v) {
|
||||
if (scr_dyn_class_reflection_fence(v)) return NULL;
|
||||
/* A plain object's prototype adds keys unless it is Object.prototype. */
|
||||
if (v->kind != SCR_DYN_OBJ || v->prototype == NULL ||
|
||||
v->prototype == scr_builtin_object_prototype) {
|
||||
ScrArr *own = scr_dyn_own_key_strs(v, true);
|
||||
if (own) return own;
|
||||
}
|
||||
return scr_dyn_key_strs_from(scr_dyn_for_in_keys(v));
|
||||
}
|
||||
|
||||
/* Object.keys as a native string[] (+1): a fresh array the caller owns. */
|
||||
ScrArr *scr_dyn_obj_key_strs(const ScrDyn *v) {
|
||||
if (scr_dyn_class_reflection_fence(v)) return NULL;
|
||||
ScrArr *own = scr_dyn_own_key_strs(v, false);
|
||||
if (own) return own;
|
||||
return scr_dyn_key_strs_from(scr_dyn_obj_keys(v));
|
||||
}
|
||||
|
||||
/* One source's own enumerable members onto an OBJ target. OBJ sources
|
||||
* snapshot their keys, then check attributes and read values in order so
|
||||
* getters can affect later entries. Arrays, strings, and bytes use their
|
||||
|
||||
@@ -4706,6 +4706,12 @@ typedef struct ScrDynTypedRecordOps {
|
||||
ScrStr *const *names; /* immortal field-name strings */
|
||||
bool (*has)(void *rec, size_t index);
|
||||
ScrDyn *(*get)(void *rec, size_t index);
|
||||
/* Every field is required: `has` always answers true and no key of the
|
||||
* record can be deleted. */
|
||||
bool fixed;
|
||||
/* The field names as an immortal string[], built on first use by
|
||||
* for-in over a record with every field present (atomic slot). */
|
||||
ScrArr *keys;
|
||||
} ScrDynTypedRecordOps;
|
||||
|
||||
/* Prototype members of a compiled class seen as `unknown`, emitted per
|
||||
@@ -4903,6 +4909,11 @@ bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
|
||||
* TypeError. */
|
||||
ScrDyn *scr_dyn_obj_keys(const ScrDyn *v);
|
||||
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v);
|
||||
/* for-in keys as a native string[] (+1); NULL with an exception pending. */
|
||||
ScrArr *scr_dyn_for_in_key_strs(const ScrDyn *v);
|
||||
bool scr_dyn_for_in_stable(const ScrDyn *v);
|
||||
/* Object.keys as a native string[] (+1); NULL with an exception pending. */
|
||||
ScrArr *scr_dyn_obj_key_strs(const ScrDyn *v);
|
||||
/* Snapshot all own string keys of a SCR_DYN_OBJ in JS order. Returns +1. */
|
||||
ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v);
|
||||
ScrDyn *scr_dyn_own_keys(const ScrDyn *value);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// for-in over typed records seen as unknown or any: key order, optional
|
||||
// fields, loop control and every binding form.
|
||||
const keysIn = (o: unknown): string => {
|
||||
const out: string[] = [];
|
||||
for (const k in o as Record<string, unknown>) out.push(k);
|
||||
return out.join(",");
|
||||
};
|
||||
type Row = { id: number; name: string; tags: string[]; nested: { a: number; b: number } };
|
||||
const row: Row = { id: 1, name: "r", tags: ["x"], nested: { a: 1, b: 2 } };
|
||||
console.log("row", keysIn(row), keysIn(row.nested), keysIn(row.tags));
|
||||
type Opt = { zeta: number; alpha?: string; mid: boolean; extra?: number[] };
|
||||
const opts: Opt[] = [
|
||||
{ zeta: 1, mid: true },
|
||||
{ zeta: 2, alpha: "a", mid: false },
|
||||
{ zeta: 3, mid: true, extra: [1] },
|
||||
{ zeta: 4, alpha: undefined, mid: false },
|
||||
];
|
||||
for (const o of opts) console.log("opt", keysIn(o));
|
||||
const numeric = { b: 1, "2": 2, a: 3, "1": 4, "10": 5, "-1": 6, "01": 7 };
|
||||
console.log("numeric", keysIn(numeric));
|
||||
|
||||
// The same shape walked many times, and two shapes interleaved.
|
||||
let total = 0;
|
||||
const other = { p: 1, q: 2, r: 3 };
|
||||
for (let i = 0; i < 1000; i++) total += keysIn(i % 2 ? row : other).length;
|
||||
console.log("repeat", total, keysIn(row), keysIn(other));
|
||||
|
||||
// break, continue, labels, nested loops.
|
||||
function walk(o: unknown): string {
|
||||
const out: string[] = [];
|
||||
outer: for (const k in o as Record<string, unknown>) {
|
||||
if (k === "name") continue;
|
||||
for (const j in o as Record<string, unknown>) {
|
||||
if (j === k) continue outer;
|
||||
if (j === "tags") break;
|
||||
out.push(k + ":" + j);
|
||||
}
|
||||
if (k === "nested") break;
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
console.log("control", walk(row));
|
||||
|
||||
// Binding forms: var, a pre-declared let, and reading values by key.
|
||||
function forms(o: unknown): string {
|
||||
const rec = o as Record<string, unknown>;
|
||||
let last = "";
|
||||
for (last in rec) {
|
||||
// assigns the outer binding
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (var k in rec) out.push(k + "=" + JSON.stringify(rec[k]));
|
||||
return last + "|" + out.join(";");
|
||||
}
|
||||
console.log("forms", forms(row));
|
||||
|
||||
// any receivers.
|
||||
const loose: any = row;
|
||||
const anyKeys: string[] = [];
|
||||
for (const k in loose) anyKeys.push(k);
|
||||
console.log("any", anyKeys.join(","));
|
||||
@@ -0,0 +1,38 @@
|
||||
// Object.keys over values seen as unknown: typed records, optional
|
||||
// fields, arrays, strings, nullish receivers, and independent results.
|
||||
const keysOf = (o: unknown): string[] => Object.keys(o as object);
|
||||
type Point = { x: number; y: number; label?: string };
|
||||
const p: Point = { x: 1, y: 2 };
|
||||
const q: Point = { x: 3, y: 4, label: "q" };
|
||||
console.log(keysOf(p).join(","), keysOf(q).join(","), keysOf(p).length + keysOf(q).length);
|
||||
console.log(keysOf([5, 6, 7]).join(","), keysOf("hey").join(","), keysOf(42).length, keysOf(true).length);
|
||||
console.log(keysOf({ b: 1, "3": 2, a: 3, "1": 4 }).join(","));
|
||||
|
||||
// Each call returns a new array the caller may change.
|
||||
const first = keysOf(p);
|
||||
first.push("added");
|
||||
first[0] = "changed";
|
||||
console.log(first.join(","), keysOf(p).join(","), keysOf({ ...p }).join(","));
|
||||
const sorted = keysOf(q).sort();
|
||||
console.log(sorted.join(","), keysOf(q).join(","));
|
||||
|
||||
for (const bad of [null, undefined]) {
|
||||
try {
|
||||
keysOf(bad);
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// Keys feed ordinary string operations.
|
||||
const joined = keysOf(q)
|
||||
.map((k) => k.toUpperCase())
|
||||
.filter((k) => k !== "Y")
|
||||
.join("-");
|
||||
console.log(joined, keysOf(q).includes("label"), keysOf(q).indexOf("y"));
|
||||
|
||||
class Box {
|
||||
width = 2;
|
||||
height = 3;
|
||||
}
|
||||
console.log(keysOf(new Box()).join(","));
|
||||
@@ -0,0 +1,33 @@
|
||||
// for-in and Object.keys over checked-dynamic objects: JSON values,
|
||||
// prototypes, null-prototype objects, and keys added or deleted mid-walk.
|
||||
const parsed: any = JSON.parse('{"b":1,"2":2,"a":3,"1":4,"nested":{"z":1,"y":2}}');
|
||||
const keysIn = (o: unknown): string => {
|
||||
const out: string[] = [];
|
||||
for (const k in o as Record<string, unknown>) out.push(k);
|
||||
return out.join(",");
|
||||
};
|
||||
console.log(keysIn(parsed), keysIn(parsed.nested), Object.keys(parsed).join(","));
|
||||
|
||||
const visited: string[] = [];
|
||||
for (const k in parsed) {
|
||||
visited.push(k);
|
||||
if (k === "1") {
|
||||
delete parsed.a;
|
||||
parsed.added = true;
|
||||
}
|
||||
}
|
||||
console.log("mutating", visited.join(","), Object.keys(parsed).join(","));
|
||||
|
||||
const bare = Object.create(null);
|
||||
bare.z = 1;
|
||||
bare["5"] = 2;
|
||||
bare.y = 3;
|
||||
console.log("null proto", keysIn(bare), Object.keys(bare).join(","));
|
||||
|
||||
const list: any = JSON.parse("[10, 20, 30]");
|
||||
console.log("array", keysIn(list), Object.keys(list).join(","));
|
||||
|
||||
let count = 0;
|
||||
for (let i = 0; i < 2000; i++) for (const k in parsed) count += k.length;
|
||||
console.log("repeat", count);
|
||||
console.log("empty", keysIn(JSON.parse("{}")), "[" + keysIn(null) + "]", "[" + keysIn(undefined) + "]");
|
||||
@@ -0,0 +1,23 @@
|
||||
// JavaScript for-in and Object.keys: results stay ordinary arrays that
|
||||
// accept any value, and dynamic objects keep JS key order.
|
||||
function keysIn(o) {
|
||||
const out = [];
|
||||
for (const k in o) out.push(k);
|
||||
return out;
|
||||
}
|
||||
const rec = { zeta: 1, alpha: 2, 3: "three", 1: "one" };
|
||||
console.log(keysIn(rec).join(","), Object.keys(rec).join(","));
|
||||
const ks = Object.keys(rec);
|
||||
ks.push(42, null);
|
||||
ks.unshift({ k: 1 });
|
||||
console.log(JSON.stringify(ks), Object.keys(rec).length);
|
||||
const parsed = JSON.parse('{"b":1,"a":2}');
|
||||
const pk = Object.keys(parsed);
|
||||
pk[5] = "gap";
|
||||
console.log(pk.length, JSON.stringify(pk));
|
||||
console.log(keysIn([1, 2]).join(","), keysIn("ab").join(","));
|
||||
function Legacy() {
|
||||
this.own = 1;
|
||||
}
|
||||
Legacy.prototype.shared = 2;
|
||||
console.log(keysIn(new Legacy()).join(","), Object.keys(new Legacy()).join(","));
|
||||
@@ -0,0 +1,33 @@
|
||||
// for-in over typed values crossing an unknown boundary: records in
|
||||
// arrays and maps, generic helpers, and records with every field optional.
|
||||
type Partial3 = { a?: number; b?: string; c?: boolean };
|
||||
const partials: Partial3[] = [{}, { a: 1 }, { b: "x", c: true }, { a: 1, b: "y", c: false }];
|
||||
function keyList(o: unknown): string {
|
||||
const out: string[] = [];
|
||||
for (const k in o as Record<string, unknown>) out.push(k);
|
||||
return "[" + out.join(",") + "]";
|
||||
}
|
||||
console.log(partials.map(keyList).join(" "));
|
||||
|
||||
function describe<T extends object>(value: T): string {
|
||||
return keyList(value) + Object.keys(value as unknown as object).length;
|
||||
}
|
||||
console.log(describe({ one: 1 }), describe({ two: 2, three: [3] }));
|
||||
|
||||
const byName = new Map<string, { id: number; tag: string }>([
|
||||
["a", { id: 1, tag: "t" }],
|
||||
["b", { id: 2, tag: "u" }],
|
||||
]);
|
||||
for (const [name, v] of byName) console.log(name, keyList(v));
|
||||
|
||||
// Writes through the unknown view are visible to later walks.
|
||||
const live: { x: number; y?: number } = { x: 1 };
|
||||
const view = live as unknown as Record<string, unknown>;
|
||||
console.log(keyList(view));
|
||||
live.y = 2;
|
||||
console.log(keyList(view), Object.keys(view).join(","));
|
||||
|
||||
let sum = 0;
|
||||
const shapes = [{ k1: 1 }, { k1: 1, k2: 2 }, { k3: 3 }];
|
||||
for (let i = 0; i < 3000; i++) for (const k in shapes[i % 3] as Record<string, unknown>) sum += k.length;
|
||||
console.log("sum", sum);
|
||||
Reference in New Issue
Block a user