Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/codegen/expressions/method-calls/string-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ function dispatchArrayReorder(
if (method === "sort") return ctx.arrayGen.generateArraySort(expr, params);
if (method === "splice") return ctx.arrayGen.generateArraySplice(expr, params);
if (method === "fill") return ctx.arrayGen.generateArrayFill(expr, params);
if (method === "copyWithin") return ctx.arrayGen.generateArrayCopyWithin(expr, params);
return null;
}

Expand Down
3 changes: 3 additions & 0 deletions src/codegen/infrastructure/generator-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export interface IArrayGenerator {
generateArraySort(expr: MethodCallNode, params: string[]): string;
generateArraySplice(expr: MethodCallNode, params: string[]): string;
generateArrayFill(expr: MethodCallNode, params: string[]): string;
generateArrayCopyWithin(expr: MethodCallNode, params: string[]): string;
}

/**
Expand Down Expand Up @@ -2102,6 +2103,8 @@ export class MockGeneratorContext implements IGeneratorContext {
generateArraySort: (_expr: MethodCallNode, _params: string[]): string => "%mock_array_sort",
generateArraySplice: (_expr: MethodCallNode, _params: string[]): string => "%mock_array_splice",
generateArrayFill: (_expr: MethodCallNode, _params: string[]): string => "%mock_array_fill",
generateArrayCopyWithin: (_expr: MethodCallNode, _params: string[]): string =>
"%mock_array_copywithin",
};

resolveImportAlias(localName: string): string {
Expand Down
11 changes: 10 additions & 1 deletion src/codegen/types/collections/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { IGeneratorContext } from "../../infrastructure/generator-context.js";
// Array sub-modules
// No alias — ChadScript doesn't resolve import aliases in self-hosting
import { generateArrayLiteral } from "./array/literal.js";
import { generateArrayPush, generateArrayPop, generateArrayFill } from "./array/mutators.js";
import {
generateArrayPush,
generateArrayPop,
generateArrayFill,
generateArrayCopyWithin,
} from "./array/mutators.js";
import { generateArrayReverse, generateArrayShift, generateArrayUnshift } from "./array/reorder.js";
import {
generateArrayIndexOf,
Expand Down Expand Up @@ -156,4 +161,8 @@ export class ArrayGenerator {
generateArrayFill(expr: MethodCallNode, params: string[]): string {
return generateArrayFill(this.ctx, expr, params);
}

generateArrayCopyWithin(expr: MethodCallNode, params: string[]): string {
return generateArrayCopyWithin(this.ctx, expr, params);
}
}
156 changes: 151 additions & 5 deletions src/codegen/types/collections/array/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,19 +715,28 @@ export function generateArrayFill(
return generateNumericArrayFillImpl(gen, expr, params, arrayPtr, fillValue);
}

function clampFillIndex(gen: IGeneratorContext, rawDouble: string, length: string): string {
function resolveArrayIndex(gen: IGeneratorContext, rawDouble: string, length: string): string {
const dbl = gen.ensureDouble(rawDouble);
const i32Val = gen.nextTemp();
gen.emit(`${i32Val} = fptosi double ${dbl} to i32`);
const isNeg = gen.emitIcmp("slt", "i32", i32Val, "0");
const clamped = gen.nextTemp();
gen.emit(`${clamped} = select i1 ${isNeg}, i32 0, i32 ${i32Val}`);
const tooHigh = gen.emitIcmp("sgt", "i32", clamped, length);
const resolved = gen.nextTemp();
gen.emit(`${resolved} = add i32 ${i32Val}, ${length}`);
const resolvedNeg = gen.emitIcmp("slt", "i32", resolved, "0");
const zeroClamp = gen.nextTemp();
gen.emit(`${zeroClamp} = select i1 ${resolvedNeg}, i32 0, i32 ${resolved}`);
const fromNeg = gen.nextTemp();
gen.emit(`${fromNeg} = select i1 ${isNeg}, i32 ${zeroClamp}, i32 ${i32Val}`);
const tooHigh = gen.emitIcmp("sgt", "i32", fromNeg, length);
const result = gen.nextTemp();
gen.emit(`${result} = select i1 ${tooHigh}, i32 ${length}, i32 ${clamped}`);
gen.emit(`${result} = select i1 ${tooHigh}, i32 ${length}, i32 ${fromNeg}`);
return result;
}

function clampFillIndex(gen: IGeneratorContext, rawDouble: string, length: string): string {
return resolveArrayIndex(gen, rawDouble, length);
}

function generateNumericArrayFillImpl(
gen: IGeneratorContext,
expr: MethodCallNode,
Expand Down Expand Up @@ -837,3 +846,140 @@ function generateStringArrayFillImpl(
gen.setVariableType(arrayPtr, "%StringArray*");
return arrayPtr;
}

export function generateArrayCopyWithin(
gen: IGeneratorContext,
expr: MethodCallNode,
params: string[],
): string {
if (expr.args.length < 2 || expr.args.length > 3) {
return gen.emitError("copyWithin() requires 2-3 arguments (target, start, end?)", expr.loc);
}

const arrayPtr = gen.generateExpression(expr.object, params);

let isStringArray = false;
const exprObjBase = expr.object as ExprBase;
if (exprObjBase.type === "variable") {
const varName = (expr.object as VariableNode).name;
const varType = gen.getVariableType(varName);
isStringArray = varType === "%StringArray*" || varType === "%StringArray";
}
if (!isStringArray) {
const ptrType = gen.getVariableType(arrayPtr);
if (ptrType === "%StringArray*" || ptrType === "%StringArray") isStringArray = true;
}

if (isStringArray) {
return generateStringArrayCopyWithinImpl(gen, expr, params, arrayPtr);
}
return generateNumericArrayCopyWithinImpl(gen, expr, params, arrayPtr);
}

function generateNumericArrayCopyWithinImpl(
gen: IGeneratorContext,
expr: MethodCallNode,
params: string[],
arrayPtr: string,
): string {
const lenPtr = gen.nextTemp();
gen.emit(`${lenPtr} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 1`);
const length = gen.emitLoad("i32", lenPtr);

const dataPtrField = gen.nextTemp();
gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`);
const dataPtr = gen.emitLoad("double*", dataPtrField);

const target = resolveArrayIndex(gen, gen.generateExpression(expr.args[0], params), length);
const start = resolveArrayIndex(gen, gen.generateExpression(expr.args[1], params), length);
const end =
expr.args.length >= 3
? resolveArrayIndex(gen, gen.generateExpression(expr.args[2], params), length)
: length;

const count = gen.nextTemp();
gen.emit(`${count} = sub i32 ${end}, ${start}`);
const remaining = gen.nextTemp();
gen.emit(`${remaining} = sub i32 ${length}, ${target}`);
const useCount = gen.emitIcmp("slt", "i32", count, remaining);
const actualCount = gen.nextTemp();
gen.emit(`${actualCount} = select i1 ${useCount}, i32 ${count}, i32 ${remaining}`);
const isPos = gen.emitIcmp("sgt", "i32", actualCount, "0");
const finalCount = gen.nextTemp();
gen.emit(`${finalCount} = select i1 ${isPos}, i32 ${actualCount}, i32 0`);

const srcPtr = gen.nextTemp();
gen.emit(`${srcPtr} = getelementptr inbounds double, double* ${dataPtr}, i32 ${start}`);
const dstPtr = gen.nextTemp();
gen.emit(`${dstPtr} = getelementptr inbounds double, double* ${dataPtr}, i32 ${target}`);

const srcI8 = gen.emitBitcast(srcPtr, "double*", "i8*");
const dstI8 = gen.emitBitcast(dstPtr, "double*", "i8*");

const byteCount = gen.nextTemp();
gen.emit(`${byteCount} = mul i32 ${finalCount}, 8`);
const byteCount64 = gen.nextTemp();
gen.emit(`${byteCount64} = zext i32 ${byteCount} to i64`);
gen.emit(
`call void @llvm.memmove.p0i8.p0i8.i64(i8* ${dstI8}, i8* ${srcI8}, i64 ${byteCount64}, i1 false)`,
);

gen.setVariableType(arrayPtr, "%Array*");
return arrayPtr;
}

function generateStringArrayCopyWithinImpl(
gen: IGeneratorContext,
expr: MethodCallNode,
params: string[],
arrayPtr: string,
): string {
const lenPtr = gen.nextTemp();
gen.emit(
`${lenPtr} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 1`,
);
const length = gen.emitLoad("i32", lenPtr);

const dataPtrField = gen.nextTemp();
gen.emit(
`${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`,
);
const dataPtr = gen.emitLoad("i8**", dataPtrField);

const target = resolveArrayIndex(gen, gen.generateExpression(expr.args[0], params), length);
const start = resolveArrayIndex(gen, gen.generateExpression(expr.args[1], params), length);
const end =
expr.args.length >= 3
? resolveArrayIndex(gen, gen.generateExpression(expr.args[2], params), length)
: length;

const count = gen.nextTemp();
gen.emit(`${count} = sub i32 ${end}, ${start}`);
const remaining = gen.nextTemp();
gen.emit(`${remaining} = sub i32 ${length}, ${target}`);
const useCount = gen.emitIcmp("slt", "i32", count, remaining);
const actualCount = gen.nextTemp();
gen.emit(`${actualCount} = select i1 ${useCount}, i32 ${count}, i32 ${remaining}`);
const isPos = gen.emitIcmp("sgt", "i32", actualCount, "0");
const finalCount = gen.nextTemp();
gen.emit(`${finalCount} = select i1 ${isPos}, i32 ${actualCount}, i32 0`);

const srcPtr = gen.nextTemp();
gen.emit(`${srcPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${start}`);
const dstPtr = gen.nextTemp();
gen.emit(`${dstPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${target}`);

const srcI8 = gen.emitBitcast(srcPtr, "i8**", "i8*");
const dstI8 = gen.emitBitcast(dstPtr, "i8**", "i8*");

const byteCount = gen.nextTemp();
gen.emit(`${byteCount} = mul i32 ${finalCount}, 8`);
const byteCount64 = gen.nextTemp();
gen.emit(`${byteCount64} = zext i32 ${byteCount} to i64`);
gen.emit(
`call void @llvm.memmove.p0i8.p0i8.i64(i8* ${dstI8}, i8* ${srcI8}, i64 ${byteCount64}, i1 false)`,
);

gen.setVariableType(arrayPtr, "%StringArray*");
return arrayPtr;
}
57 changes: 57 additions & 0 deletions tests/fixtures/arrays/array-copywithin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
function testCopyWithin(): void {
const nums: number[] = [1, 2, 3, 4, 5];
nums.copyWithin(0, 3);
if (nums[0] !== 4) {
console.log("FAIL: expected nums[0]=4 got " + nums[0]);
process.exit(1);
}
if (nums[1] !== 5) {
console.log("FAIL: expected nums[1]=5 got " + nums[1]);
process.exit(1);
}
if (nums[2] !== 3) {
console.log("FAIL: expected nums[2]=3 got " + nums[2]);
process.exit(1);
}

const nums2: number[] = [1, 2, 3, 4, 5];
nums2.copyWithin(1, 3, 4);
if (nums2[0] !== 1) {
console.log("FAIL: expected nums2[0]=1 got " + nums2[0]);
process.exit(1);
}
if (nums2[1] !== 4) {
console.log("FAIL: expected nums2[1]=4 got " + nums2[1]);
process.exit(1);
}
if (nums2[2] !== 3) {
console.log("FAIL: expected nums2[2]=3 got " + nums2[2]);
process.exit(1);
}

const strs: string[] = ["a", "b", "c", "d", "e"];
strs.copyWithin(0, 3);
if (strs[0] !== "d") {
console.log("FAIL: expected strs[0]='d' got '" + strs[0] + "'");
process.exit(1);
}
if (strs[1] !== "e") {
console.log("FAIL: expected strs[1]='e' got '" + strs[1] + "'");
process.exit(1);
}

const nums3: number[] = [1, 2, 3, 4, 5];
nums3.copyWithin(1, 0, 2);
if (nums3[1] !== 1) {
console.log("FAIL: expected nums3[1]=1 got " + nums3[1]);
process.exit(1);
}
if (nums3[2] !== 2) {
console.log("FAIL: expected nums3[2]=2 got " + nums3[2]);
process.exit(1);
}

console.log("TEST_PASSED");
}

testCopyWithin();
Loading