diff --git a/src/codegen/expressions/method-calls/string-methods.ts b/src/codegen/expressions/method-calls/string-methods.ts index 60d477ea..81e376fd 100644 --- a/src/codegen/expressions/method-calls/string-methods.ts +++ b/src/codegen/expressions/method-calls/string-methods.ts @@ -546,7 +546,7 @@ export function handleReplace( const replaceArg = expr.args[1]; if (searchArg.type === "regex") { - const regexNode = searchArg as { pattern: string; flags: string }; + const regexNode = searchArg as { type: string; pattern: string; flags: string }; const isGlobal = regexNode.flags.indexOf("g") !== -1; const searchStr = ctx.stringGen.doGenerateGlobalString(regexNode.pattern); const replaceStr = ctx.generateExpression(replaceArg, params); diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 5265d742..6f19323a 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -1810,7 +1810,7 @@ export class TypeInference { let e = expr as ExprBase; let indexExpr: Expression = expr; if (e.type === "type_assertion") { - const assertion = expr as { expression: Expression; assertedType: string }; + const assertion = expr as { type: string; expression: Expression; assertedType: string }; if (assertion.expression) { indexExpr = assertion.expression; e = assertion.expression as ExprBase; diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index be46d08b..b4c73668 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -1667,7 +1667,12 @@ export class VariableAllocator { if (!setTypeInfoResult && stmt.value) { const valueBase = stmt.value as { type: string }; if (valueBase.type === "new") { - const newExpr = stmt.value as { className: string; typeArgs?: string[] }; + const newExpr = stmt.value as { + type: string; + className: string; + args: Expression[]; + typeArgs?: string[]; + }; if (newExpr.className === "Set" && newExpr.typeArgs && newExpr.typeArgs.length > 0) { setTypeInfoResult = { valueType: newExpr.typeArgs[0] }; } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index f3d86dab..87a7c285 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -120,6 +120,7 @@ import { JsonObjectMeta } from "./expressions/access/member.js"; import type { TargetInfo } from "../target-types.js"; import { checkClosureMutations } from "../semantic/closure-mutation-checker.js"; import { checkUnionTypes } from "../semantic/union-type-checker.js"; +import { checkTypeAssertions } from "../semantic/type-assertion-checker.js"; export interface SemaSymbolData { names: string[]; @@ -2351,6 +2352,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { generateParts(): string[] { checkClosureMutations(this.ast); checkUnionTypes(this.ast); + checkTypeAssertions(this.ast); const irParts: string[] = []; diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 61a5ea5c..36a30a8c 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -327,7 +327,7 @@ function handleExpressionStatement(node: TreeSitterNode, ast: AST): void { const e = expr as ExprBase; if (e.type === "member_access_assignment" || e.type === "index_access_assignment") { - const memberExprTyped = expr as { type: string; property: string }; + const memberExprTyped = expr as { type: string; object: Expression; property: string }; const assignment: AssignmentStatement = { type: "assignment", name: diff --git a/src/semantic/type-assertion-checker.ts b/src/semantic/type-assertion-checker.ts new file mode 100644 index 00000000..7ad23e74 --- /dev/null +++ b/src/semantic/type-assertion-checker.ts @@ -0,0 +1,457 @@ +// Type assertion field-order checker — semantic pass run before IR generation. +// Inline type assertions like (expr as { type: string; left: Expr; right: Expr }) use their listed +// field positions as GEP indices in native code. If those fields are not a prefix of the real +// interface in the correct order, the GEP reads wrong memory → segfault. +// +// This pass finds all inline { } type assertions with 2+ fields, looks for declared interfaces +// that contain all those field names, and errors if none of those interfaces accept the assertion +// as a valid consecutive prefix (in-order, no gaps). + +import type { + AST, + Statement, + Expression, + BlockStatement, + VariableDeclaration, + AssignmentStatement, + IfStatement, + WhileStatement, + DoWhileStatement, + ForStatement, + ForOfStatement, + TryStatement, + SwitchStatement, + ReturnStatement, + ThrowStatement, + ArrowFunctionNode, + TypeAssertionNode, + InterfaceDeclaration, + InterfaceField, + ObjectProperty, + MapEntry, + MethodCallNode, +} from "../ast/types.js"; + +export function checkTypeAssertions(ast: AST): void { + const checker = new TypeAssertionChecker(ast); + checker.check(); +} + +class TypeAssertionChecker { + private ast: AST; + + constructor(ast: AST) { + this.ast = ast; + } + + check(): void { + if (this.ast.topLevelItems && this.ast.topLevelItems.length > 0) { + this.walkStatements(this.ast.topLevelItems as Statement[]); + } + for (let i = 0; i < this.ast.functions.length; i++) { + this.walkBlock(this.ast.functions[i].body); + } + for (let i = 0; i < this.ast.classes.length; i++) { + const cls = this.ast.classes[i]; + for (let j = 0; j < cls.methods.length; j++) { + this.walkBlock(cls.methods[j].body); + } + } + } + + private walkStatements(stmts: Statement[]): void { + for (let i = 0; i < stmts.length; i++) { + this.walkStatement(stmts[i]); + } + } + + private walkBlock(block: BlockStatement): void { + this.walkStatements(block.statements); + } + + private walkStatement(stmt: Statement): void { + const s = stmt as { type: string }; + const stype = s.type; + if (stype === "variable_declaration") { + const decl = stmt as VariableDeclaration; + if (decl.value) this.checkExpr(decl.value as Expression); + } else if (stype === "assignment") { + const asgn = stmt as AssignmentStatement; + this.checkExpr(asgn.value); + } else if (stype === "if") { + const ifStmt = stmt as IfStatement; + this.checkExpr(ifStmt.condition); + this.walkBlock(ifStmt.thenBlock); + if (ifStmt.elseBlock) this.walkBlock(ifStmt.elseBlock); + } else if (stype === "while") { + const whileStmt = stmt as WhileStatement; + this.checkExpr(whileStmt.condition); + this.walkBlock(whileStmt.body); + } else if (stype === "do_while") { + const doWhile = stmt as DoWhileStatement; + this.walkBlock(doWhile.body); + this.checkExpr(doWhile.condition); + } else if (stype === "for") { + const forStmt = stmt as ForStatement; + if (forStmt.init) this.walkStatement(forStmt.init as Statement); + if (forStmt.condition) this.checkExpr(forStmt.condition as Expression); + this.walkBlock(forStmt.body); + if (forStmt.update) this.checkExpr(forStmt.update as Expression); + } else if (stype === "for_of") { + const forOf = stmt as ForOfStatement; + this.checkExpr(forOf.iterable); + this.walkBlock(forOf.body); + } else if (stype === "try") { + const tryStmt = stmt as TryStatement; + this.walkBlock(tryStmt.tryBlock); + if (tryStmt.catchBody) this.walkBlock(tryStmt.catchBody); + if (tryStmt.finallyBlock) this.walkBlock(tryStmt.finallyBlock); + } else if (stype === "switch") { + const switchStmt = stmt as SwitchStatement; + this.checkExpr(switchStmt.discriminant); + for (let i = 0; i < switchStmt.cases.length; i++) { + const c = switchStmt.cases[i]; + if (c.test) this.checkExpr(c.test as Expression); + this.walkStatements(c.consequent); + } + } else if (stype === "return") { + const retStmt = stmt as ReturnStatement; + if (retStmt.value) this.checkExpr(retStmt.value as Expression); + } else if (stype === "throw") { + const throwStmt = stmt as ThrowStatement; + this.checkExpr(throwStmt.argument); + } else if (stype === "block") { + this.walkBlock(stmt as BlockStatement); + } else if (stype !== "break" && stype !== "continue") { + this.checkExpr(stmt as Expression); + } + } + + private checkExpr(expr: Expression): void { + const e = expr as { type: string }; + const etype = e.type; + if (etype === "type_assertion") { + const ta = expr as TypeAssertionNode; + this.validateInlineAssertion(ta); + this.checkExpr(ta.expression); + } else if (etype === "binary") { + // BinaryNode: { type, op, left, right } + const bin = expr as { type: string; op: string; left: Expression; right: Expression }; + this.checkExpr(bin.left); + this.checkExpr(bin.right); + } else if (etype === "unary") { + // UnaryNode: { type, op, operand } + const unary = expr as { type: string; op: string; operand: Expression }; + this.checkExpr(unary.operand); + } else if (etype === "call") { + // CallNode: { type, name, args } + const call = expr as { type: string; name: string; args: Expression[] }; + for (let i = 0; i < call.args.length; i++) { + this.checkExpr(call.args[i]); + } + } else if (etype === "method_call") { + const mc = expr as MethodCallNode; + this.checkExpr(mc.object); + for (let i = 0; i < mc.args.length; i++) { + this.checkExpr(mc.args[i]); + } + } else if (etype === "member_access") { + const ma = expr as { type: string; object: Expression }; + this.checkExpr(ma.object); + } else if (etype === "index_access") { + // IndexAccessNode: { type, object, index } + const ia = expr as { type: string; object: Expression; index: Expression }; + this.checkExpr(ia.object); + this.checkExpr(ia.index); + } else if (etype === "array") { + const arr = expr as { type: string; elements: Expression[] }; + for (let i = 0; i < arr.elements.length; i++) { + this.checkExpr(arr.elements[i]); + } + } else if (etype === "object") { + const obj = expr as { type: string; properties: ObjectProperty[] }; + for (let i = 0; i < obj.properties.length; i++) { + const prop = obj.properties[i] as ObjectProperty; + this.checkExpr(prop.value); + } + } else if (etype === "conditional") { + const cond = expr as { + type: string; + condition: Expression; + consequent: Expression; + alternate: Expression; + }; + this.checkExpr(cond.condition); + this.checkExpr(cond.consequent); + this.checkExpr(cond.alternate); + } else if (etype === "await") { + const aw = expr as { type: string; argument: Expression }; + this.checkExpr(aw.argument); + } else if (etype === "new") { + // NewNode: { type, className, args } + const newExpr = expr as { type: string; className: string; args: Expression[] }; + for (let i = 0; i < newExpr.args.length; i++) { + this.checkExpr(newExpr.args[i]); + } + } else if (etype === "arrow_function") { + const arrow = expr as ArrowFunctionNode; + const bodyTyped = arrow.body as { type: string }; + if (bodyTyped.type === "block") { + this.walkBlock(arrow.body as BlockStatement); + } else { + this.checkExpr(arrow.body as Expression); + } + } else if (etype === "template_literal") { + const tl = expr as { type: string; parts: (string | Expression)[] }; + for (let i = 0; i < tl.parts.length; i++) { + const part = tl.parts[i]; + const partTyped = part as { type: string }; + if (partTyped.type) { + this.checkExpr(part as Expression); + } + } + } else if (etype === "spread_element") { + const se = expr as { type: string; argument: Expression }; + this.checkExpr(se.argument); + } else if (etype === "member_access_assignment") { + // MemberAccessAssignmentNode: { type, object, property, value } + const maa = expr as { + type: string; + object: Expression; + property: string; + value: Expression; + }; + this.checkExpr(maa.object); + this.checkExpr(maa.value); + } else if (etype === "index_access_assignment") { + // IndexAccessAssignmentNode: { type, object, index, value } + const iaa = expr as { + type: string; + object: Expression; + index: Expression; + value: Expression; + }; + this.checkExpr(iaa.object); + this.checkExpr(iaa.index); + this.checkExpr(iaa.value); + } else if (etype === "map") { + const mapExpr = expr as { type: string; entries: MapEntry[] }; + for (let i = 0; i < mapExpr.entries.length; i++) { + const entry = mapExpr.entries[i] as MapEntry; + this.checkExpr(entry.key); + this.checkExpr(entry.value); + } + } else if (etype === "set") { + const setExpr = expr as { type: string; values: Expression[] }; + for (let i = 0; i < setExpr.values.length; i++) { + this.checkExpr(setExpr.values[i]); + } + } + } + + private validateInlineAssertion(ta: TypeAssertionNode): void { + const assertedType = ta.assertedType; + if (!assertedType.startsWith("{")) return; + + const parsedFields = this.parseInlineFields(assertedType); + if (parsedFields.length < 2) return; + + const assertedNames: string[] = []; + for (let i = 0; i < parsedFields.length; i++) { + const pf = parsedFields[i] as InterfaceField; + assertedNames.push(this.stripOpt(pf.name)); + } + + if (!this.ast.interfaces) return; + + let anyMatchingInterface = false; + let anyValidPrefix = false; + let bestMatchIface: InterfaceDeclaration | null = null; + let bestMatchFields: InterfaceField[] | null = null; + let bestMatchReason: string | null = null; + + for (let i = 0; i < this.ast.interfaces.length; i++) { + const iface = this.ast.interfaces[i] as InterfaceDeclaration; + const allFields = this.getAllFields(iface); + + const indices: number[] = []; + let allFound = true; + for (let j = 0; j < assertedNames.length; j++) { + let idx = -1; + for (let k = 0; k < allFields.length; k++) { + const f = allFields[k] as InterfaceField; + if (this.stripOpt(f.name) === assertedNames[j]) { + idx = k; + break; + } + } + if (idx === -1) { + allFound = false; + break; + } + indices.push(idx); + } + if (!allFound) continue; + + anyMatchingInterface = true; + + let orderOk = true; + for (let j = 1; j < indices.length; j++) { + if (indices[j] <= indices[j - 1]) { + orderOk = false; + break; + } + } + if (!orderOk) { + if (bestMatchIface === null) { + bestMatchIface = iface; + bestMatchFields = allFields; + bestMatchReason = "fields are out of order relative to '" + iface.name + "'"; + } + continue; + } + + const maxIdx = indices[indices.length - 1]; + let missingField: string | null = null; + for (let k = 0; k < maxIdx; k++) { + const f = allFields[k] as InterfaceField; + const fname = this.stripOpt(f.name); + if (assertedNames.indexOf(fname) === -1) { + missingField = fname; + break; + } + } + if (missingField !== null) { + if (bestMatchIface === null) { + bestMatchIface = iface; + bestMatchFields = allFields; + bestMatchReason = + "field '" + + missingField + + "' from '" + + iface.name + + "' is skipped before the last listed field"; + } + continue; + } + + anyValidPrefix = true; + break; + } + + if (anyMatchingInterface && !anyValidPrefix && bestMatchIface !== null) { + this.reportError(ta, bestMatchIface, bestMatchFields!, assertedNames, bestMatchReason!); + } + } + + private getAllFields(iface: InterfaceDeclaration): InterfaceField[] { + const result: InterfaceField[] = []; + if (iface.extends && iface.extends.length > 0) { + for (let i = 0; i < iface.extends.length; i++) { + const parentName = iface.extends[i]; + const parent = this.getInterface(parentName); + if (parent) { + const parentFields = this.getAllFields(parent); + for (let j = 0; j < parentFields.length; j++) { + result.push(parentFields[j]); + } + } + } + } + for (let i = 0; i < iface.fields.length; i++) { + result.push(iface.fields[i]); + } + return result; + } + + private getInterface(name: string): InterfaceDeclaration | null { + if (!this.ast.interfaces) return null; + for (let i = 0; i < this.ast.interfaces.length; i++) { + const iface = this.ast.interfaces[i] as InterfaceDeclaration; + if (iface.name === name) return iface; + } + return null; + } + + private reportError( + ta: TypeAssertionNode, + iface: InterfaceDeclaration, + ifaceFields: InterfaceField[], + assertedNames: string[], + reason: string, + ): void { + let msg = ""; + if (ta.loc) { + const file = ta.loc.file || ""; + msg += file + ":" + ta.loc.line + ":" + (ta.loc.column + 1) + ": error: "; + } else { + msg += "error: "; + } + msg += + "inline type assertion fields do not form a valid prefix of '" + + iface.name + + "': " + + reason + + "\n"; + msg += " assertion: { " + assertedNames.join("; ") + " }\n"; + const ifaceFieldNames: string[] = []; + for (let i = 0; i < ifaceFields.length; i++) { + ifaceFieldNames.push((ifaceFields[i] as InterfaceField).name); + } + msg += " interface: { " + ifaceFieldNames.join("; ") + " }\n"; + msg += " note: inline assertion field positions define GEP indices in native code\n"; + msg += + " hint: use 'as " + iface.name + "' or list fields as a prefix in exact interface order\n"; + console.error(msg); + process.exit(1); + } + + // Parse "{ name: type; name2: type2 }" into InterfaceField pairs. Handles nested generics. + // Class method (not standalone) so the native compiler can track the InterfaceField[] return type. + private parseInlineFields(typeStr: string): InterfaceField[] { + if (!typeStr.startsWith("{") || !typeStr.endsWith("}")) return []; + const inner = typeStr.slice(1, typeStr.length - 1).trim(); + if (!inner) return []; + const fields: InterfaceField[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < inner.length; i++) { + const ch = inner[i]; + if (ch === "{" || ch === "(" || ch === "[" || ch === "<") { + depth++; + } else if (ch === "}" || ch === ")" || ch === "]" || ch === ">") { + depth--; + } else if (ch === ";" && depth === 0) { + const part = inner.slice(start, i).trim(); + if (part) { + const colonIdx = part.indexOf(":"); + if (colonIdx !== -1) { + const field: InterfaceField = { + name: part.slice(0, colonIdx).trim(), + type: part.slice(colonIdx + 1).trim(), + }; + fields.push(field); + } + } + start = i + 1; + } + } + const lastPart = inner.slice(start).trim(); + if (lastPart) { + const colonIdx = lastPart.indexOf(":"); + if (colonIdx !== -1) { + const field: InterfaceField = { + name: lastPart.slice(0, colonIdx).trim(), + type: lastPart.slice(colonIdx + 1).trim(), + }; + fields.push(field); + } + } + return fields; + } + + private stripOpt(name: string): string { + if (name.endsWith("?")) return name.slice(0, name.length - 1); + return name; + } +}