From 354c25b70debd713fd8ccf89550324713a09853c Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 11:17:03 -0800 Subject: [PATCH 01/10] jsx support --- c_bridges/treesitter-bridge.c | 4 +- src/chad-native.ts | 2 + src/compiler.ts | 32 +++++- src/native-compiler-lib.ts | 10 +- src/parser-native/transformer.ts | 133 +++++++++++++++++++++++ src/parser-ts/handlers/expressions.ts | 139 ++++++++++++++++++++++++ src/parser-ts/index.ts | 11 +- tests/fixtures/jsx/jsx-attributes.tsx | 21 ++++ tests/fixtures/jsx/jsx-basic.tsx | 11 ++ tests/fixtures/jsx/jsx-expression.tsx | 18 +++ tests/fixtures/jsx/jsx-fragment.tsx | 17 +++ tests/fixtures/jsx/jsx-nested.tsx | 17 +++ tests/fixtures/jsx/jsx-self-closing.tsx | 11 ++ tests/test-discovery.ts | 6 +- 14 files changed, 415 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/jsx/jsx-attributes.tsx create mode 100644 tests/fixtures/jsx/jsx-basic.tsx create mode 100644 tests/fixtures/jsx/jsx-expression.tsx create mode 100644 tests/fixtures/jsx/jsx-fragment.tsx create mode 100644 tests/fixtures/jsx/jsx-nested.tsx create mode 100644 tests/fixtures/jsx/jsx-self-closing.tsx diff --git a/c_bridges/treesitter-bridge.c b/c_bridges/treesitter-bridge.c index 744db4dc..61b8cbae 100644 --- a/c_bridges/treesitter-bridge.c +++ b/c_bridges/treesitter-bridge.c @@ -3,13 +3,13 @@ #include #include -extern TSLanguage *tree_sitter_typescript(void); +extern TSLanguage *tree_sitter_tsx(void); extern void *GC_malloc_uncollectable(size_t size); extern void *GC_malloc_atomic(size_t size); TSTree *__ts_parse_source(const char *source, uint32_t length) { TSParser *parser = ts_parser_new(); - TSLanguage *lang = tree_sitter_typescript(); + TSLanguage *lang = tree_sitter_tsx(); ts_parser_set_language(parser, lang); return ts_parser_parse_string(parser, NULL, source, length); } diff --git a/src/chad-native.ts b/src/chad-native.ts index 45f798ac..9750ce37 100644 --- a/src/chad-native.ts +++ b/src/chad-native.ts @@ -202,6 +202,8 @@ let outputFile: string = ".build/" + inputForOutput; const explicitOutput = parser.getOption("output"); if (explicitOutput.length > 0) { outputFile = explicitOutput; +} else if (inputForOutput.substr(inputForOutput.length - 4) === ".tsx") { + outputFile = ".build/" + inputForOutput.substr(0, inputForOutput.length - 4); } else if (inputForOutput.substr(inputForOutput.length - 3) === ".ts") { outputFile = ".build/" + inputForOutput.substr(0, inputForOutput.length - 3); } else if (inputForOutput.substr(inputForOutput.length - 3) === ".js") { diff --git a/src/compiler.ts b/src/compiler.ts index 8aeeb22b..508cd31c 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -115,7 +115,10 @@ const PICOHTTPPARSER_PATH = process.env.CHADSCRIPT_PICOHTTPPARSER_PATH || "./ven const YYJSON_PATH = process.env.CHADSCRIPT_YYJSON_PATH || "./vendor/yyjson"; const LIBUV_PATH = process.env.CHADSCRIPT_LIBUV_PATH || "./vendor/libuv/build"; const TREESITTER_LIB_PATH = process.env.CHADSCRIPT_TREESITTER_PATH || "./vendor/tree-sitter"; -const TREESITTER_TS_PATH = "node_modules/tree-sitter-typescript/typescript/src"; +// TSX grammar is a strict superset of TypeScript — all .ts code parses identically. +// The only difference: expr angle-bracket assertions become JSX, but ChadScript +// uses `as Type` so there's no impact on existing code. +const TREESITTER_TS_PATH = "node_modules/tree-sitter-typescript/tsx/src"; // ============================================ // MAIN COMPILER DRIVER @@ -220,13 +223,13 @@ export function compile( // Create TypeScript type checker if compiling .ts files let typeChecker: TypeChecker | null = null; - if (inputFile.endsWith(".ts")) { + if (inputFile.endsWith(".ts") || inputFile.endsWith(".tsx")) { try { const files: { filename: string; code: string }[] = []; for (let fci = 0; fci < fileContentKeys.length; fci++) { const filename = fileContentKeys[fci]; const code = fileContentValues[fci]; - if (filename.endsWith(".ts")) { + if (filename.endsWith(".ts") || filename.endsWith(".tsx")) { files.push({ filename, code }); } } @@ -747,12 +750,30 @@ function resolveImportPath(fromFile: string, importSource: string): string { const dir = path.dirname(fromFile); const resolved = path.resolve(dir, importSource); - // If the import has .js extension, prefer .ts source over compiled .js + // If the import has .js extension, prefer .ts/.tsx source over compiled .js if (importSource.endsWith(".js")) { const tsPath = resolved.replace(/\.js$/, ".ts"); if (fs.existsSync(tsPath)) { return tsPath; } + const tsxPath = resolved.replace(/\.js$/, ".tsx"); + if (fs.existsSync(tsxPath)) { + return tsxPath; + } + } + + // Extensionless imports: try .ts then .tsx + if ( + !importSource.endsWith(".ts") && + !importSource.endsWith(".tsx") && + !importSource.endsWith(".js") + ) { + if (fs.existsSync(resolved + ".ts")) { + return resolved + ".ts"; + } + if (fs.existsSync(resolved + ".tsx")) { + return resolved + ".tsx"; + } } return resolved; @@ -770,9 +791,12 @@ function resolveNodeModule(fromFile: string, packageName: string): string | null const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")); const entryPoints = [ pkgJson.main?.replace(/\.js$/, ".ts"), + pkgJson.main?.replace(/\.js$/, ".tsx"), pkgJson.main?.replace(/\.js$/, ""), "index.ts", + "index.tsx", "src/index.ts", + "src/index.tsx", ].filter(Boolean); for (const entry of entryPoints) { diff --git a/src/native-compiler-lib.ts b/src/native-compiler-lib.ts index 38d59178..2dd0c848 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -578,12 +578,16 @@ export function resolveImportPath(fromFile: string, importSource: string): strin const dir = path.dirname(fromFile); const resolved = path.resolve(dir + "/" + importSource); - // Prefer .ts source over compiled .js + // Prefer .ts/.tsx source over compiled .js if (importSource.substr(importSource.length - 3) === ".js") { const tsPath = resolved.substr(0, resolved.length - 3) + ".ts"; if (fs.existsSync(tsPath)) { return tsPath; } + const tsxPath = resolved.substr(0, resolved.length - 3) + ".tsx"; + if (fs.existsSync(tsxPath)) { + return tsxPath; + } } if (fs.existsSync(resolved)) { @@ -594,6 +598,10 @@ export function resolveImportPath(fromFile: string, importSource: string): strin return resolved + ".ts"; } + if (fs.existsSync(resolved + ".tsx")) { + return resolved + ".tsx"; + } + if (fs.existsSync(resolved + ".js")) { return resolved + ".js"; } diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index cfd7294b..3224fe3f 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -483,11 +483,144 @@ function transformExpression(node: TreeSitterNode): Expression { case "typeof_expression": return transformTypeofExpression(node); + // JSX desugaring — convert JSX syntax to createElement() calls + case "jsx_element": + return transformJsxElementNative(node); + + case "jsx_self_closing_element": + return transformJsxSelfClosingElementNative(node); + + case "jsx_expression": + // Bare JSX expression container — unwrap to the inner expression + const jsxInner = getNamedChild(node, 0); + return jsxInner ? transformExpression(jsxInner) : { type: "variable", name: "undefined" }; + default: return { type: "variable", name: "undefined" }; } } +// ============================================ +// JSX DESUGARING (native parser) +// Mirrors the TS-API parser's JSX desugaring. +// Tree-sitter TSX grammar node types: +// jsx_element: has open_tag (jsx_opening_element) and close_tag (jsx_closing_element) +// jsx_self_closing_element: has name + attributes, no children +// jsx_opening_element: has name field (absent for fragments) + attribute fields +// jsx_text: raw text content between tags +// jsx_expression: {expr} containers +// ============================================ + +function makeJsxCallNode(tagName: string, props: Expression, children: Expression): CallNode { + // Build args with push() — the semantic analyzer treats push()-built arrays as + // homogeneous (all Expression) whereas inline array literals with different shapes + // trigger "mixed array types" errors during self-hosting. + const args: Expression[] = []; + args.push({ type: "string", value: tagName }); + args.push(props); + args.push(children); + return { type: "call", name: "createElement", args }; +} + +function transformJsxElementNative(node: TreeSitterNode): CallNode { + const openTag = getChildByFieldName(node, "open_tag"); + let tagName = "Fragment"; + let props: Expression = { type: "object", properties: [] }; + + if (openTag && !(openTag as NodeBase).isNull) { + const nameNode = getChildByFieldName(openTag, "name"); + if (nameNode && !(nameNode as NodeBase).isNull) { + tagName = (nameNode as NodeBase).text; + } + // else: no name field means this is a fragment (<>...) + props = transformJsxAttributesNative(openTag); + } + + const children = transformJsxChildrenNative(node); + return makeJsxCallNode(tagName, props, children); +} + +function transformJsxSelfClosingElementNative(node: TreeSitterNode): CallNode { + const nameNode = getChildByFieldName(node, "name"); + const tagName = + nameNode && !(nameNode as NodeBase).isNull ? (nameNode as NodeBase).text : "Fragment"; + const props: Expression = transformJsxAttributesNative(node); + const emptyChildren: Expression = { type: "array", elements: [] }; + return makeJsxCallNode(tagName, props, emptyChildren); +} + +function transformJsxAttributesNative(node: TreeSitterNode): ObjectNode { + const properties: { key: string; value: Expression }[] = []; + const childCount = node.childCount; + + for (let i = 0; i < childCount; i++) { + const child = getChild(node, i); + if (!child) continue; + const childBase = child as NodeBase; + if (childBase.type !== "jsx_attribute") continue; + + // First named child is property_identifier (key), second is value + const keyNode = getNamedChild(child, 0); + if (!keyNode) continue; + const key = (keyNode as NodeBase).text; + + const valueNode = getNamedChild(child, 1); + let value: Expression; + + if (!valueNode || (valueNode as NodeBase).isNull) { + // Boolean shorthand: → { disabled: true } + value = { type: "boolean", value: true }; + } else { + const valueBase = valueNode as NodeBase; + if (valueBase.type === "string") { + value = transformStringNode(valueNode); + } else if (valueBase.type === "jsx_expression") { + const inner = getNamedChild(valueNode, 0); + value = inner ? transformExpression(inner) : { type: "variable", name: "undefined" }; + } else { + value = transformExpression(valueNode); + } + } + + properties.push({ key, value }); + } + + return { type: "object", properties }; +} + +function transformJsxChildrenNative(node: TreeSitterNode): ArrayNode { + const elements: Expression[] = []; + const childCount = node.namedChildCount; + + for (let i = 0; i < childCount; i++) { + const child = getNamedChild(node, i); + if (!child) continue; + const childBase = child as NodeBase; + + // Skip the open_tag and close_tag — only process content children + if (childBase.type === "jsx_opening_element" || childBase.type === "jsx_closing_element") { + continue; + } + + if (childBase.type === "jsx_text") { + const trimmed = childBase.text.trim(); + if (trimmed.length === 0) continue; + elements.push({ type: "string", value: trimmed }); + } else if (childBase.type === "jsx_expression") { + const inner = getNamedChild(child, 0); + if (inner) { + elements.push(transformExpression(inner)); + } + } else if (childBase.type === "jsx_element") { + elements.push(transformJsxElementNative(child)); + } else if (childBase.type === "jsx_self_closing_element") { + elements.push(transformJsxSelfClosingElementNative(child)); + } + } + + return { type: "array", elements }; +} + function transformTypeAssertion(node: TreeSitterNode): TypeAssertionNode { const exprChild = getNamedChild(node, 0); const expression = exprChild diff --git a/src/parser-ts/handlers/expressions.ts b/src/parser-ts/handlers/expressions.ts index bb6fbbb7..71343b17 100644 --- a/src/parser-ts/handlers/expressions.ts +++ b/src/parser-ts/handlers/expressions.ts @@ -127,6 +127,23 @@ export function transformExpression( transformExpression((node as ts.VoidExpression).expression, checker); return { type: "undefined", loc: getLoc(node) }; + // JSX desugaring — convert JSX syntax to createElement() calls + case ts.SyntaxKind.JsxElement: + return transformJsxElement(node as ts.JsxElement, checker); + + case ts.SyntaxKind.JsxSelfClosingElement: + return transformJsxSelfClosingElement(node as ts.JsxSelfClosingElement, checker); + + case ts.SyntaxKind.JsxFragment: + return transformJsxFragment(node as ts.JsxFragment, checker); + + case ts.SyntaxKind.JsxExpression: + // Bare JSX expression container — unwrap to the inner expression + if ((node as ts.JsxExpression).expression) { + return transformExpression((node as ts.JsxExpression).expression!, checker); + } + return { type: "undefined", loc: getLoc(node) }; + default: throw new Error(`Unsupported expression kind: ${ts.SyntaxKind[node.kind]}`); } @@ -699,3 +716,125 @@ function getTypeNodeText(typeNode: ts.TypeNode): string { if (typeNode.kind === ts.SyntaxKind.BooleanKeyword) return "boolean"; return typeNode.getText(); } + +// ============================================ +// JSX DESUGARING +// Converts JSX syntax to createElement() calls. +// child → createElement("Tag", { prop: v }, [child]) +// ============================================ + +function getJsxTagName(tagName: ts.JsxTagNameExpression): string { + if (ts.isIdentifier(tagName)) { + return tagName.text; + } + if (ts.isPropertyAccessExpression(tagName)) { + return tagName.getText(); + } + return tagName.getText(); +} + +function transformJsxElement(node: ts.JsxElement, checker: ts.TypeChecker | undefined): CallNode { + const tagName = getJsxTagName(node.openingElement.tagName); + const props = transformJsxAttributes(node.openingElement.attributes, checker); + const children = transformJsxChildren(node.children, checker); + + return { + type: "call", + name: "createElement", + args: [{ type: "string", value: tagName } as StringNode, props, children], + loc: getLoc(node), + }; +} + +function transformJsxSelfClosingElement( + node: ts.JsxSelfClosingElement, + checker: ts.TypeChecker | undefined, +): CallNode { + const tagName = getJsxTagName(node.tagName); + const props = transformJsxAttributes(node.attributes, checker); + + return { + type: "call", + name: "createElement", + args: [ + { type: "string", value: tagName } as StringNode, + props, + { type: "array", elements: [] } as ArrayNode, + ], + loc: getLoc(node), + }; +} + +function transformJsxFragment(node: ts.JsxFragment, checker: ts.TypeChecker | undefined): CallNode { + const children = transformJsxChildren(node.children, checker); + + return { + type: "call", + name: "createElement", + args: [ + { type: "string", value: "Fragment" } as StringNode, + { type: "object", properties: [] } as ObjectNode, + children, + ], + loc: getLoc(node), + }; +} + +function transformJsxAttributes( + attributes: ts.JsxAttributes, + checker: ts.TypeChecker | undefined, +): ObjectNode { + const properties: { key: string; value: Expression }[] = []; + + for (const attr of attributes.properties) { + if (ts.isJsxAttribute(attr)) { + // attr.name is Identifier or JsxNamespacedName — use getText() for both + const key = ts.isIdentifier(attr.name) ? attr.name.text : attr.name.getText(); + let value: Expression; + + if (!attr.initializer) { + // Boolean shorthand: → { disabled: true } + value = { type: "boolean", value: true }; + } else if (ts.isStringLiteral(attr.initializer)) { + value = { type: "string", value: attr.initializer.text }; + } else if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) { + value = transformExpression(attr.initializer.expression, checker); + } else { + value = { type: "undefined" }; + } + + properties.push({ key, value }); + } + // ts.isJsxSpreadAttribute — spread attributes out of scope for v1 + } + + return { type: "object", properties }; +} + +function transformJsxChildren( + children: ts.NodeArray, + checker: ts.TypeChecker | undefined, +): ArrayNode { + const elements: Expression[] = []; + + for (const child of children) { + if (ts.isJsxText(child)) { + // Trim whitespace-only text nodes (indentation, newlines between tags) + const trimmed = child.text.trim(); + if (trimmed.length === 0) continue; + elements.push({ type: "string", value: trimmed }); + } else if (ts.isJsxExpression(child)) { + if (child.expression) { + elements.push(transformExpression(child.expression, checker)); + } + } else if (ts.isJsxElement(child)) { + elements.push(transformJsxElement(child, checker)); + } else if (ts.isJsxSelfClosingElement(child)) { + elements.push(transformJsxSelfClosingElement(child, checker)); + } else if (ts.isJsxFragment(child)) { + elements.push(transformJsxFragment(child, checker)); + } + } + + return { type: "array", elements }; +} diff --git a/src/parser-ts/index.ts b/src/parser-ts/index.ts index 2b709572..c634bd93 100644 --- a/src/parser-ts/index.ts +++ b/src/parser-ts/index.ts @@ -9,13 +9,10 @@ interface ParseOptions { export function parseWithTSAPI(code: string, options: ParseOptions = {}): AST { const filename = options.filename || "input.ts"; - const sourceFile = ts.createSourceFile( - filename, - code, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS, - ); + // Use TSX mode for .tsx files so is parsed as JSX, not a type assertion + const scriptKind = filename.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + + const sourceFile = ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, scriptKind); return transformSourceFile(sourceFile, undefined); } diff --git a/tests/fixtures/jsx/jsx-attributes.tsx b/tests/fixtures/jsx/jsx-attributes.tsx new file mode 100644 index 00000000..e02f2ba6 --- /dev/null +++ b/tests/fixtures/jsx/jsx-attributes.tsx @@ -0,0 +1,21 @@ +// JSX attributes become object properties, boolean shorthand works +interface BoxProps { + border: string; + disabled: boolean; +} + +function createElement(tag: string, props: BoxProps, children: string[]): string { + let result = tag; + if (props.border === "single") { + result = result + ":border"; + } + if (props.disabled === true) { + result = result + ":disabled"; + } + return result; +} + +const result = ; +if (result === "Box:border:disabled") { + console.log("TEST_PASSED"); +} diff --git a/tests/fixtures/jsx/jsx-basic.tsx b/tests/fixtures/jsx/jsx-basic.tsx new file mode 100644 index 00000000..ab10f3ce --- /dev/null +++ b/tests/fixtures/jsx/jsx-basic.tsx @@ -0,0 +1,11 @@ +// Basic JSX element desugars to createElement call +interface EmptyProps {} + +function createElement(tag: string, props: EmptyProps, children: string[]): string { + return tag + ":" + children.length.toString(); +} + +const result = hello; +if (result === "Box:1") { + console.log("TEST_PASSED"); +} diff --git a/tests/fixtures/jsx/jsx-expression.tsx b/tests/fixtures/jsx/jsx-expression.tsx new file mode 100644 index 00000000..16b8e785 --- /dev/null +++ b/tests/fixtures/jsx/jsx-expression.tsx @@ -0,0 +1,18 @@ +// Expression children are evaluated as expressions +interface EmptyProps {} + +function createElement(tag: string, props: EmptyProps, children: string[]): string { + let result = tag; + let i = 0; + while (i < children.length) { + result = result + "(" + children[i] + ")"; + i = i + 1; + } + return result; +} + +const name = "world"; +const result = hello {name}; +if (result === "Text(hello)(world)") { + console.log("TEST_PASSED"); +} diff --git a/tests/fixtures/jsx/jsx-fragment.tsx b/tests/fixtures/jsx/jsx-fragment.tsx new file mode 100644 index 00000000..34454c78 --- /dev/null +++ b/tests/fixtures/jsx/jsx-fragment.tsx @@ -0,0 +1,17 @@ +// Fragments desugar to createElement("Fragment", {}, [...]) +interface EmptyProps {} + +function createElement(tag: string, props: EmptyProps, children: string[]): string { + let result = tag; + let i = 0; + while (i < children.length) { + result = result + "(" + children[i] + ")"; + i = i + 1; + } + return result; +} + +const result = <>hello world; +if (result === "Fragment(hello world)") { + console.log("TEST_PASSED"); +} diff --git a/tests/fixtures/jsx/jsx-nested.tsx b/tests/fixtures/jsx/jsx-nested.tsx new file mode 100644 index 00000000..910e5871 --- /dev/null +++ b/tests/fixtures/jsx/jsx-nested.tsx @@ -0,0 +1,17 @@ +// Nested JSX elements produce nested createElement calls +interface EmptyProps {} + +function createElement(tag: string, props: EmptyProps, children: string[]): string { + let result = tag; + let i = 0; + while (i < children.length) { + result = result + "(" + children[i] + ")"; + i = i + 1; + } + return result; +} + +const result = hello; +if (result === "Box(Text(hello))") { + console.log("TEST_PASSED"); +} diff --git a/tests/fixtures/jsx/jsx-self-closing.tsx b/tests/fixtures/jsx/jsx-self-closing.tsx new file mode 100644 index 00000000..9f23faba --- /dev/null +++ b/tests/fixtures/jsx/jsx-self-closing.tsx @@ -0,0 +1,11 @@ +// Self-closing JSX element produces empty children array +interface EmptyProps {} + +function createElement(tag: string, props: EmptyProps, children: string[]): string { + return tag + ":" + children.length.toString(); +} + +const result = ; +if (result === "Input:0") { + console.log("TEST_PASSED"); +} diff --git a/tests/test-discovery.ts b/tests/test-discovery.ts index ee5041e9..86c4384b 100644 --- a/tests/test-discovery.ts +++ b/tests/test-discovery.ts @@ -72,7 +72,7 @@ function parseAnnotations(filePath: string): ParsedAnnotations { // "string-split-length.ts" → "string split length" function filenameToDescription(filename: string): string { - const base = path.basename(filename).replace(/\.(ts|js)$/, ""); + const base = path.basename(filename).replace(/\.(tsx?|js)$/, ""); return base.replace(/[-_]/g, " "); } @@ -84,7 +84,7 @@ function collectFixtures(dir: string, base: string): string[] { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...collectFixtures(fullPath, base)); - } else if (/\.(js|ts)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) { + } else if (/\.(js|tsx?|ts)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) { results.push(path.relative(base, fullPath)); } } @@ -107,7 +107,7 @@ export function discoverTests(fixturesDir: string = "tests/fixtures"): TestCase[ // Name from relative path without extension: "arrays/array-filter" const relToFixtures = path.relative(fixturesDir, relPath); - const name = relToFixtures.replace(/\.(ts|js)$/, ""); + const name = relToFixtures.replace(/\.(tsx?|js)$/, ""); const description = annotations.description || filenameToDescription(relPath); From 055f2c195751d6839a9f2c6a134fbfc979d1e116 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 12:53:13 -0800 Subject: [PATCH 02/10] declare function support and linker flags --- .github/workflows/ci.yml | 8 +-- .github/workflows/cross-compile.yml | 2 +- src/analysis/semantic-analyzer.ts | 3 + src/ast/types.ts | 3 + src/chad-native.ts | 47 +++++++++++++++ src/chad-node.ts | 26 ++++++++ src/codegen/expressions/calls.ts | 13 ++-- src/codegen/llvm-generator.ts | 94 +++++++++++++++++++++++++---- src/compiler.ts | 22 ++++++- src/native-compiler-lib.ts | 33 +++++++++- src/parser-native/transformer.ts | 75 +++++++++++++++++++++++ src/parser-ts/transformer.ts | 21 +++++++ 12 files changed, 323 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 893b7f56..137a3dff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,10 +68,10 @@ jobs: exit 1 fi - - name: Build tree-sitter TypeScript objects + - name: Build tree-sitter TSX objects run: | mkdir -p build - TS_SRC=node_modules/tree-sitter-typescript/typescript/src + TS_SRC=node_modules/tree-sitter-typescript/tsx/src TS_INCLUDE=node_modules/tree-sitter-typescript clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o @@ -158,10 +158,10 @@ jobs: exit 1 fi - - name: Build tree-sitter TypeScript objects + - name: Build tree-sitter TSX objects run: | mkdir -p build - TS_SRC=node_modules/tree-sitter-typescript/typescript/src + TS_SRC=node_modules/tree-sitter-typescript/tsx/src TS_INCLUDE=node_modules/tree-sitter-typescript clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml index f47e9604..3c56ca52 100644 --- a/.github/workflows/cross-compile.yml +++ b/.github/workflows/cross-compile.yml @@ -37,7 +37,7 @@ jobs: - name: Build tree-sitter objects run: | mkdir -p build - TS_SRC=node_modules/tree-sitter-typescript/typescript/src + TS_SRC=node_modules/tree-sitter-typescript/tsx/src TS_INCLUDE=node_modules/tree-sitter-typescript clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o diff --git a/src/analysis/semantic-analyzer.ts b/src/analysis/semantic-analyzer.ts index 4eac934d..b1580118 100644 --- a/src/analysis/semantic-analyzer.ts +++ b/src/analysis/semantic-analyzer.ts @@ -305,6 +305,9 @@ export class SemanticAnalyzer { } private analyzeFunction(func: FunctionNode): void { + // External declarations have no body to analyze + if (func.declare) return; + this.currentFunction = func.name; this.checkFunctionUnionTypes(func.name, func.paramTypes, func.returnType); diff --git a/src/ast/types.ts b/src/ast/types.ts index 8fa50771..98abb3bb 100644 --- a/src/ast/types.ts +++ b/src/ast/types.ts @@ -409,6 +409,9 @@ export interface FunctionNode { async?: boolean; parameters?: FunctionParameter[]; loc?: SourceLocation; + // External C function declaration (declare function foo(): void) + // When true, codegen emits LLVM `declare` instead of `define`, no _cs_ prefix + declare?: boolean; } export interface ClassMethod { diff --git a/src/chad-native.ts b/src/chad-native.ts index 9750ce37..b0d2654e 100644 --- a/src/chad-native.ts +++ b/src/chad-native.ts @@ -5,6 +5,9 @@ import { setTargetCpu, setTargetTriple, setVerbose, + addLinkObj, + addLinkLib, + addLinkPath, } from "./native-compiler-lib.js"; import { getDtsContent } from "./codegen/stdlib/embedded-dts.js"; import { ArgumentParser } from "./argparse.js"; @@ -62,6 +65,21 @@ parser.addScopedOption( "build,run,ir", ); parser.addScopedOption("target-cpu", "", "Set LLVM target CPU", "native", "build,run,ir"); +parser.addScopedOption("link-obj", "", "Extra .o files to link (comma-separated)", "", "build,run"); +parser.addScopedOption( + "link-lib", + "", + "Extra libraries to link, -l flags (comma-separated)", + "", + "build,run", +); +parser.addScopedOption( + "link-path", + "", + "Extra library paths, -L flags (comma-separated)", + "", + "build,run", +); parser.addPositional("input", "Input .ts or .js file"); parser.parse(process.argv); @@ -180,6 +198,35 @@ if (cpuOpt.length > 0) { setTargetCpu(cpuOpt); } +// Parse extra linker flags (comma-separated lists) +const linkObjOpt = parser.getOption("link-obj"); +if (linkObjOpt.length > 0) { + const parts = linkObjOpt.split(","); + let _loi = 0; + while (_loi < parts.length) { + addLinkObj(parts[_loi]); + _loi = _loi + 1; + } +} +const linkLibOpt = parser.getOption("link-lib"); +if (linkLibOpt.length > 0) { + const parts = linkLibOpt.split(","); + let _lli = 0; + while (_lli < parts.length) { + addLinkLib(parts[_lli]); + _lli = _lli + 1; + } +} +const linkPathOpt = parser.getOption("link-path"); +if (linkPathOpt.length > 0) { + const parts = linkPathOpt.split(","); + let _lpi = 0; + while (_lpi < parts.length) { + addLinkPath(parts[_lpi]); + _lpi = _lpi + 1; + } +} + const inputFile = parser.getPositional(0); if (inputFile.length === 0) { console.log("chad: error: no input files"); diff --git a/src/chad-node.ts b/src/chad-node.ts index 8a4ca109..c0382b21 100644 --- a/src/chad-node.ts +++ b/src/chad-node.ts @@ -12,6 +12,9 @@ import { setTarget, setTargetCpu, setStaticLink, + addLinkObj, + addLinkLib, + addLinkPath, } from "./compiler.js"; import { LogLevel, logger } from "./utils/logger.js"; import { runInit } from "./codegen/stdlib/init-templates.js"; @@ -52,6 +55,21 @@ parser.addScopedOption( "build,run,ir", ); parser.addScopedFlag("static", "", "Link statically", "build,run"); +parser.addScopedOption("link-obj", "", "Extra .o files to link (comma-separated)", "", "build,run"); +parser.addScopedOption( + "link-lib", + "", + "Extra libraries to link, -l flags (comma-separated)", + "", + "build,run", +); +parser.addScopedOption( + "link-path", + "", + "Extra library paths, -L flags (comma-separated)", + "", + "build,run", +); parser.addPositional("input", "Input .ts or .js file"); // Node's process.argv includes [node, script, ...] — skip both. @@ -218,6 +236,14 @@ if (targetOpt) { const cpuOpt = parser.getOption("target-cpu"); if (cpuOpt) setTargetCpu(cpuOpt); +// Parse extra linker flags (comma-separated lists) +const linkObjOpt = parser.getOption("link-obj"); +if (linkObjOpt) linkObjOpt.split(",").forEach((o) => addLinkObj(o)); +const linkLibOpt = parser.getOption("link-lib"); +if (linkLibOpt) linkLibOpt.split(",").forEach((l) => addLinkLib(l)); +const linkPathOpt = parser.getOption("link-path"); +if (linkPathOpt) linkPathOpt.split(",").forEach((p) => addLinkPath(p)); + if (command === "ir") { setEmitLLVMOnly(true); setKeepTemps(true); diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index 8832732a..c93fa519 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -627,16 +627,17 @@ export class CallExpressionGenerator { } } + // Declared functions (TS `declare function`) are external C symbols — + // use their real name without the _cs_ prefix + const mangledName = + func && func.declare ? resolvedFuncName : this.ctx.mangleUserName(resolvedFuncName); + if (returnType === "void") { - this.ctx.emitCallVoid(`@${this.ctx.mangleUserName(resolvedFuncName)}`, argsList.join(", ")); + this.ctx.emitCallVoid(`@${mangledName}`, argsList.join(", ")); return "0"; } - const temp = this.ctx.emitCall( - returnType, - `@${this.ctx.mangleUserName(resolvedFuncName)}`, - argsList.join(", "), - ); + const temp = this.ctx.emitCall(returnType, `@${mangledName}`, argsList.join(", ")); return temp; } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index c1465c48..d3d512da 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -33,6 +33,7 @@ import { AwaitExpressionNode, BinaryNode, SourceLocation, + FunctionParameter, } from "../ast/types.js"; import { BaseGenerator, SymbolKind, SymbolTable } from "./infrastructure/base-generator.js"; import { @@ -76,6 +77,8 @@ import { stripNullable, tsTypeToLlvm, tsTypeToLlvmJson, + mapReturnTypeToLLVM, + mapParamTypeToLLVM, } from "./infrastructure/type-system.js"; import type { ResolvedType } from "./infrastructure/type-system.js"; import { DiagnosticEngine } from "../diagnostics/engine.js"; @@ -1413,6 +1416,9 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { private typeAliasesCount: number = 0; private usesTreeSitter: boolean = false; + // Tracks function names from user `declare function` to avoid duplicate + // LLVM declarations when a name overlaps with hardcoded runtime declarations + public declaredExternFunctions: Set; public sourceCode: string = ""; public filename: string = ""; @@ -1421,6 +1427,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { // Initialize complex fields in constructor (field initializers don't work in native code) this.externalFunctions = new Set(); + this.declaredExternFunctions = new Set(); this.topLevelObjectVariables = new Map(); this.globalVariables = new Map(); this.importAliasNames = []; @@ -2517,7 +2524,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { if (this.usesTreeSitter) { const tsDecls = this.treesitterGen.generateDeclarations(); if (tsDecls) { - irParts.push(tsDecls); + irParts.push(this.filterDuplicateDeclarations(tsDecls)); } irParts.push("\n"); } @@ -2576,13 +2583,15 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { } finalParts.push( - getLLVMDeclarations({ - curl: this.usesCurl !== 0, - crypto: this.usesCrypto !== 0, - sqlite: this.usesSqlite !== 0, - testRunner: this.usesTestRunner !== 0, - targetOS: this.getTargetOS(), - }), + this.filterDuplicateDeclarations( + getLLVMDeclarations({ + curl: this.usesCurl !== 0, + crypto: this.usesCrypto !== 0, + sqlite: this.usesSqlite !== 0, + testRunner: this.usesTestRunner !== 0, + targetOS: this.getTargetOS(), + }), + ), ); if (this.usesCurl) { @@ -2608,7 +2617,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { if (this.usesMongoose) { const httpServerDecls = this.httpServerGen.generateDeclarations(); if (httpServerDecls) { - finalParts.push(httpServerDecls); + finalParts.push(this.filterDuplicateDeclarations(httpServerDecls)); } finalParts.push("\n"); } @@ -2621,7 +2630,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { if (needsLibuv) { const libuvDecls = this.libuvGen.generateDeclarations(this.usesCurl !== 0); if (libuvDecls) { - finalParts.push(libuvDecls); + finalParts.push(this.filterDuplicateDeclarations(libuvDecls)); } finalParts.push("\n"); } @@ -2629,7 +2638,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { if (needsPromise) { const promiseDecls = this.promiseGen.generateDeclarations(); if (promiseDecls) { - finalParts.push(promiseDecls); + finalParts.push(this.filterDuplicateDeclarations(promiseDecls)); } finalParts.push("\n"); } @@ -2689,6 +2698,36 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { return finalParts; } + /** + * Remove `declare` lines whose function name is already in the + * user-declared extern set (from `declare function` in TS source). + * Prevents LLVM "invalid redefinition" errors when hardcoded runtime + * declarations overlap with user declarations. + */ + private filterDuplicateDeclarations(ir: string): string { + if (this.declaredExternFunctions.size === 0) return ir; + const lines = ir.split("\n"); + const filtered: string[] = []; + for (let dli = 0; dli < lines.length; dli++) { + const line = lines[dli]; + if (line.startsWith("declare ")) { + const atIdx = line.indexOf("@"); + if (atIdx !== -1) { + const rest = line.substring(atIdx + 1); + const parenIdx = rest.indexOf("("); + if (parenIdx !== -1) { + const fnName = rest.substring(0, parenIdx); + if (this.declaredExternFunctions.has(fnName)) { + continue; + } + } + } + } + filtered.push(line); + } + return filtered.join("\n"); + } + /** * Generates LLVM IR for a function declaration and implementation. * Handles parameter types, allocas, body code generation, and return. @@ -2723,6 +2762,11 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { } private generateFunction(func: FunctionNode): string { + // External C function declaration: emit LLVM `declare` with correct types, + // no _cs_ prefix (external symbols use their real C names) + if (func.declare) { + return this.generateDeclareFunction(func); + } if (this.debugInfoEnabled && func.name) { const line = this.getLocLine(func); this.currentSubprogramId = this.dbgCreateSubprogram(func.name, line); @@ -2733,6 +2777,34 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { return ir; } + /** Emit an LLVM `declare` for an external C function (from TS `declare function`). */ + private generateDeclareFunction(func: FunctionNode): string { + const retType = func.returnType + ? mapReturnTypeToLLVM(func.returnType, this.isEnumType(func.returnType)) + : "double"; + + const paramLlvmTypes: string[] = []; + if (func.paramTypes) { + for (let i = 0; i < func.paramTypes.length; i++) { + const pt = func.paramTypes[i]; + paramLlvmTypes.push( + mapParamTypeToLLVM(pt, func.params[i] || "", this.isEnumType(stripNullable(pt)), false), + ); + } + } else if (func.parameters) { + for (let i = 0; i < func.parameters.length; i++) { + const p = func.parameters[i] as FunctionParameter; + const pType = p.type || "number"; + paramLlvmTypes.push( + mapParamTypeToLLVM(pType, p.name || "", this.isEnumType(stripNullable(pType)), false), + ); + } + } + + this.declaredExternFunctions.add(func.name); + return `declare ${retType} @${func.name}(${paramLlvmTypes.join(", ")})\n`; + } + /** * Allocate stack space for a variable declaration. * Handles all variable types: strings, arrays, objects, maps, sets, regex, classes, Response, etc. diff --git a/src/compiler.ts b/src/compiler.ts index 508cd31c..661d1208 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -75,6 +75,10 @@ let debugInfo = false; let staticLink = false; let targetCpu = "native"; let targetOverride: TargetInfo | null = null; +// Extra linker flags from --link-obj, --link-lib, --link-path +let extraLinkObjs: string[] = []; +let extraLinkLibs: string[] = []; +let extraLinkPaths: string[] = []; export function setTargetCpu(value: string): void { targetCpu = value; @@ -108,6 +112,18 @@ export function setTarget(value: string): void { targetOverride = resolveTarget(value); } +export function addLinkObj(objPath: string): void { + extraLinkObjs.push(objPath); +} + +export function addLinkLib(lib: string): void { + extraLinkLibs.push(lib); +} + +export function addLinkPath(libPath: string): void { + extraLinkPaths.push(libPath); +} + // External library paths - check env vars, then use vendor/ const BDWGC_PATH = process.env.CHADSCRIPT_BDWGC_PATH || "./vendor/bdwgc"; const LWS_BRIDGE_PATH = process.env.CHADSCRIPT_LWS_BRIDGE_PATH || "./c_bridges"; @@ -452,7 +468,11 @@ export function compile( // Homebrew's clang can't find lld by short name on macOS CI runners. // Try ld.lld first (ELF-specific), fall back to lld (multicall binary, auto-detects format). const crossLinker = crossCompiling ? ` -fuse-ld=${findLLD()}` : ""; - const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${cpSpawnObj}${extraObjs} -o ${outputFile}${noPie}${debugFlag}${staticFlag}${crossTarget}${crossLinker}${sanitizeFlags} ${linkLibs}`; + // User-provided linker flags (--link-obj, --link-lib, --link-path) + const userObjs = extraLinkObjs.length > 0 ? " " + extraLinkObjs.join(" ") : ""; + const userPaths = extraLinkPaths.map((p) => ` -L${p}`).join(""); + const userLibs = extraLinkLibs.map((l) => ` -l${l}`).join(""); + const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${cpSpawnObj}${extraObjs}${userObjs} -o ${outputFile}${noPie}${debugFlag}${staticFlag}${crossTarget}${crossLinker}${sanitizeFlags} ${linkLibs}${userPaths}${userLibs}`; logger.info(` ${linkCmd}`); const linkStdio = logger.getLevel() >= LogLevel.Verbose ? "inherit" : "pipe"; execSync(linkCmd, { stdio: linkStdio }); diff --git a/src/native-compiler-lib.ts b/src/native-compiler-lib.ts index 2dd0c848..1cd7af16 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -78,6 +78,10 @@ export let emitLLVMOnly = false; export let verbose = false; export let targetCpu = "native"; export let targetTriple = ""; +// Extra linker flags from --link-obj, --link-lib, --link-path +export let extraLinkObjs: string[] = []; +export let extraLinkLibs: string[] = []; +export let extraLinkPaths: string[] = []; export function setSkipSemanticAnalysis(value: boolean): void { skipSemanticAnalysis = value; @@ -99,6 +103,18 @@ export function setTargetTriple(value: string): void { targetTriple = value; } +export function addLinkObj(objPath: string): void { + extraLinkObjs.push(objPath); +} + +export function addLinkLib(lib: string): void { + extraLinkLibs.push(lib); +} + +export function addLinkPath(libPath: string): void { + extraLinkPaths.push(libPath); +} + // Resolve the home directory for SDK lookups function getHomeDir(): string { // Use HOME env var directly — works in the native runtime @@ -448,6 +464,19 @@ export function compileNative(inputFile: string, outputFile: string): void { // Try ld.lld first (ELF-specific), fall back to lld (multicall binary, auto-detects format). const crossLinker = crossCompiling ? " -fuse-ld=" + findLLD() : ""; + // User-provided linker flags (--link-obj, --link-lib, --link-path) + let userLinkObjs = ""; + for (let _oi = 0; _oi < extraLinkObjs.length; _oi++) { + userLinkObjs = userLinkObjs + " " + extraLinkObjs[_oi]; + } + let userLinkFlags = ""; + for (let _pi = 0; _pi < extraLinkPaths.length; _pi++) { + userLinkFlags = userLinkFlags + " -L" + extraLinkPaths[_pi]; + } + for (let _li = 0; _li < extraLinkLibs.length; _li++) { + userLinkFlags = userLinkFlags + " -l" + extraLinkLibs[_li]; + } + const linkCmd = clangTool + " " + @@ -468,6 +497,7 @@ export function compileNative(inputFile: string, outputFile: string): void { cpSpawnObj + " " + treeSitterObjs + + userLinkObjs + " -o " + outputFile + noPie + @@ -475,7 +505,8 @@ export function compileNative(inputFile: string, outputFile: string): void { crossTargetFlag + crossLinker + " " + - linkLibs; + linkLibs + + userLinkFlags; if (verbose) { console.log("Running: " + linkCmd); } diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 3224fe3f..63a02fc4 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -233,7 +233,75 @@ function transformTopLevelNode(node: TreeSitterNode, ast: AST): void { case "export_statement": handleExportStatement(node, ast); break; + + // `declare function foo(x: string): string` — tree-sitter wraps it in + // ambient_declaration containing a function_signature child (same fields + // as function_declaration minus the body). + // NOTE: no block braces — ChadScript's switch codegen drops block-scoped cases. + case "ambient_declaration": + handleAmbientDeclaration(node, ast); + break; + } +} + +// Extract declared functions from `declare function` statements. +// Uses text parsing instead of tree-sitter child navigation to avoid +// native FFI crashes with function_signature nodes. +function handleAmbientDeclaration(node: TreeSitterNode, ast: AST): void { + const nb = node as NodeBase; + const text = nb.text; + const fIdx = text.indexOf("function "); + if (fIdx === -1) return; + const rest = text.substring(fIdx + 9); + const openParen = rest.indexOf("("); + if (openParen === -1) return; + const funcName = rest.substring(0, openParen); + + const closeParen = rest.indexOf(")"); + if (closeParen === -1) return; + const paramStr = rest.substring(openParen + 1, closeParen); + + let returnType = "void"; + const afterClose = rest.substring(closeParen + 1); + const retColon = afterClose.indexOf(":"); + if (retColon !== -1) { + let retStr = afterClose.substring(retColon + 1); + retStr = retStr.trim(); + const semi = retStr.indexOf(";"); + if (semi !== -1) retStr = retStr.substring(0, semi); + returnType = retStr; + } + + const paramNames: string[] = []; + const paramTypesList: string[] = []; + if (paramStr.length > 0) { + const parts = paramStr.split(","); + let pi = 0; + while (pi < parts.length) { + const part = parts[pi].trim(); + if (part.length > 0) { + const pColon = part.indexOf(":"); + if (pColon !== -1) { + paramNames.push(part.substring(0, pColon).trim()); + paramTypesList.push(part.substring(pColon + 1).trim()); + } + } + pi = pi + 1; + } } + + const func: FunctionNode = { + name: funcName, + params: paramNames, + body: createEmptyBlock(), + returnType: returnType, + paramTypes: paramTypesList, + typeParameters: undefined, + async: undefined, + parameters: undefined, + declare: true, + }; + ast.functions.push(func); } function handleExpressionStatement(node: TreeSitterNode, ast: AST): void { @@ -2209,6 +2277,12 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null const paramTypes = paramsNode ? extractParamTypes(paramsNode) : undefined; const parameters = paramsNode ? extractFunctionParameters(paramsNode) : undefined; + // Include `declare: false` so this creation site matches the struct layout + // of handleAmbientDeclaration's creation site (which has declare: true). + // Native code determines struct layout from object literals — all creation + // sites for the same interface must have the same fields. + // IMPORTANT: must be `false` not `undefined` — native codegen may skip + // the field slot entirely for `undefined`, causing struct size mismatch. return { name, params, @@ -2218,6 +2292,7 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null typeParameters, async: isAsync || undefined, parameters, + declare: false, }; } diff --git a/src/parser-ts/transformer.ts b/src/parser-ts/transformer.ts index 3a5117b4..a54b4d74 100644 --- a/src/parser-ts/transformer.ts +++ b/src/parser-ts/transformer.ts @@ -20,6 +20,7 @@ import { MemberAccessNode, IndexAccessNode, SourceLocation, + FunctionNode, } from "../ast/types.js"; import { transformExpression } from "./handlers/expressions.js"; @@ -89,6 +90,26 @@ function transformTopLevelStatement( const funcDecl = node as ts.FunctionDeclaration; const isDeclare = funcDecl.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword); if (isDeclare) { + // Preserve declared functions in the AST so codegen can emit + // LLVM `declare` statements and use correct types for calls. + // Must create a new object with `declare` in the literal — native + // code only allocates struct slots for fields in object literals. + const func = transformFunctionDeclaration(funcDecl, checker); + if (func) { + const declFunc: FunctionNode = { + name: func.name, + params: func.params, + body: func.body, + returnType: func.returnType, + paramTypes: func.paramTypes, + typeParameters: func.typeParameters, + async: func.async, + parameters: func.parameters, + loc: func.loc, + declare: true, + }; + ast.functions.push(declFunc); + } break; } const func = transformFunctionDeclaration(funcDecl, checker); From 1505c2bb6cb5d7e5fa680efbd3fa5126a375e455 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 12:53:19 -0800 Subject: [PATCH 03/10] add zireael tui example --- examples/tui/README.md | 40 ++++ examples/tui/app.tsx | 69 +++++++ examples/tui/build.sh | 50 +++++ examples/tui/zireael-bridge.c | 345 ++++++++++++++++++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 examples/tui/README.md create mode 100644 examples/tui/app.tsx create mode 100755 examples/tui/build.sh create mode 100644 examples/tui/zireael-bridge.c diff --git a/examples/tui/README.md b/examples/tui/README.md new file mode 100644 index 00000000..2cd22a37 --- /dev/null +++ b/examples/tui/README.md @@ -0,0 +1,40 @@ +# TUI Demo (Zireael) + +Interactive terminal UI app compiled to a native binary with ChadScript. + +Uses [Zireael](https://github.com/nicegraf/Zireael) — a C11 terminal rendering engine with a binary drawlist protocol. + +## Prerequisites + +- Zireael cloned and built (set `ZIREAEL_DIR` env var to point to it) +- CMake + clang + +## Build & Run + +```bash +ZIREAEL_DIR=../Zireael bash examples/tui/build.sh +.build/examples/tui/app +``` + +## Controls + +- **UP/DOWN** — increment/decrement counter by 1 +- **LEFT/RIGHT** — increment/decrement counter by 10 +- **ESC** — quit + +## How It Works + +``` +TypeScript (app.tsx) + │ declare function zr_*() — typed FFI declarations + ▼ +C bridge (zireael-bridge.c) + │ builds drawlist byte buffers, parses event batches + ▼ +libzireael.a + │ diffs framebuffer, flushes terminal escape codes + ▼ +Terminal +``` + +The `declare function` syntax tells ChadScript to emit correct LLVM IR calls to external C functions. The `--link-obj` flag links the bridge and Zireael library into the final binary. diff --git a/examples/tui/app.tsx b/examples/tui/app.tsx new file mode 100644 index 00000000..a67f8b33 --- /dev/null +++ b/examples/tui/app.tsx @@ -0,0 +1,69 @@ +// ChadScript TUI Demo — Zireael-powered native terminal app +// +// A simple interactive counter demonstrating the full render loop: +// poll events → update state → build drawlist → present +// +// Build: bash examples/tui/build.sh +// Run: .build/examples/tui/app + +// FFI: zireael-bridge.c — engine lifecycle +declare function zr_init(): string; +declare function zr_destroy(engine: string): void; + +// FFI: zireael-bridge.c — event polling (returns "key:escape", "key:up", etc.) +declare function zr_poll(engine: string, timeout_ms: number): string; + +// FFI: zireael-bridge.c — drawlist construction +declare function zr_begin(engine: string): void; +declare function zr_clear(engine: string): void; +declare function zr_fill_rect(engine: string, x: number, y: number, w: number, h: number, fg: number, bg: number): void; +declare function zr_draw_text(engine: string, x: number, y: number, text: string, fg: number, bg: number): void; +declare function zr_present(engine: string): number; + +// Colors (0x00RRGGBB) +const WHITE = 0xFFFFFF; +const CYAN = 0x00FFFF; +const GRAY = 0x888888; +const DARK_BLUE = 0x002244; +const BLACK = 0x000000; +const GREEN = 0x00FF88; +const YELLOW = 0xFFCC00; + +const engine = zr_init(); +let count = 0; +let running = true; + +while (running) { + const event = zr_poll(engine, 16); + console.log(event); + if (event === "key:escape") { + running = false; + } else if (event === "key:up") { + count = count + 1; + } else if (event === "key:down") { + count = count - 1; + } else if (event === "key:right") { + count = count + 10; + } else if (event === "key:left") { + count = count - 10; + } + + zr_begin(engine); + zr_clear(engine); + + // Header bar + zr_fill_rect(engine, 0, 0, 50, 1, WHITE, DARK_BLUE); + zr_draw_text(engine, 2, 0, "ChadScript TUI Demo", CYAN, DARK_BLUE); + + // Counter display + zr_draw_text(engine, 2, 2, "Counter: " + count.toString(), GREEN, GREEN); + + // Instructions + zr_draw_text(engine, 2, 4, "UP/DOWN +/- 1", GRAY, BLACK); + zr_draw_text(engine, 2, 5, "LEFT/RIGHT +/- 10", GRAY, BLACK); + zr_draw_text(engine, 2, 6, "ESC quit", GRAY, BLACK); + + zr_present(engine); +} + +zr_destroy(engine); diff --git a/examples/tui/build.sh b/examples/tui/build.sh new file mode 100755 index 00000000..d2fd0108 --- /dev/null +++ b/examples/tui/build.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Build the ChadScript TUI demo (Zireael-powered terminal app). +# +# This uses `chad build` with --link-obj/--link-path/--link-lib to compile +# the TypeScript app, link the C bridge, and produce a native binary. +# +# Usage: bash examples/tui/build.sh +# Run: .build/examples/tui/app + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +ZIREAEL_DIR="${ZIREAEL_DIR:?Set ZIREAEL_DIR to point to your Zireael checkout}" +ZIREAEL_LIB="$ZIREAEL_DIR/out/build/posix-clang-release/libzireael.a" +ZIREAEL_INCLUDE="$ZIREAEL_DIR/include" +BRIDGE_SRC="$SCRIPT_DIR/zireael-bridge.c" +BRIDGE_OBJ="$SCRIPT_DIR/zireael-bridge.o" + +# --- Step 1: Build Zireael if needed --- +if [ ! -f "$ZIREAEL_LIB" ]; then + echo "Building Zireael..." + (cd "$ZIREAEL_DIR" && cmake --preset posix-clang-release && cmake --build --preset posix-clang-release) +fi + +# --- Step 2: Compile the C bridge --- +echo "Compiling zireael-bridge.c..." +clang -c -O2 -fPIC \ + -I "$ZIREAEL_INCLUDE" \ + "$BRIDGE_SRC" \ + -o "$BRIDGE_OBJ" + +# --- Step 3: Build the app with chad --- +echo "Building app.tsx..." +cd "$REPO_DIR" + +# Use node compiler (dist/chad-node.js) if native chad isn't available +CHAD="node dist/chad-node.js" +if [ -f ".build/chad" ]; then + CHAD=".build/chad" +fi + +$CHAD build examples/tui/app.tsx \ + -o .build/examples/tui/app \ + --link-obj "$BRIDGE_OBJ,$ZIREAEL_LIB" \ + --link-lib pthread + +echo "" +echo "Built: .build/examples/tui/app" +echo "Run: .build/examples/tui/app" diff --git a/examples/tui/zireael-bridge.c b/examples/tui/zireael-bridge.c new file mode 100644 index 00000000..e63abbcd --- /dev/null +++ b/examples/tui/zireael-bridge.c @@ -0,0 +1,345 @@ +/* + zireael-bridge.c — C bridge between ChadScript and Zireael TUI engine. + + With proper `declare function` support, ChadScript calls external C + functions by their real names (no _cs_ prefix). TS `declare function + zr_init()` emits `call @zr_init()` in the IR, matching these C names. + + The bridge manages drawlist construction internally: TypeScript calls + begin/clear/fill_rect/draw_text/present, and the bridge serializes the + binary drawlist format that Zireael expects. +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* GC_malloc for returning strings to ChadScript (GC-managed, no leaks) */ +extern void *GC_malloc_atomic(size_t size); + +/* --- Little-endian helpers (Zireael wire format is LE) --- */ + +static inline void le16w(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v); + p[1] = (uint8_t)(v >> 8u); +} + +static inline void le32w(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v); + p[1] = (uint8_t)(v >> 8u); + p[2] = (uint8_t)(v >> 16u); + p[3] = (uint8_t)(v >> 24u); +} + +static inline uint32_t le32r(const uint8_t *p) { + return ((uint32_t)p[0]) | ((uint32_t)p[1] << 8u) | ((uint32_t)p[2] << 16u) | + ((uint32_t)p[3] << 24u); +} + +static inline uint32_t align4(uint32_t x) { return (x + 3u) & ~3u; } + +/* --- Drawlist builder state (reset each frame by zr_begin) --- */ + +#define CMD_BUF_CAP 16384 +#define STRING_POOL_CAP 8192 +#define MAX_STRINGS 128 + +static uint8_t g_cmd_buf[CMD_BUF_CAP]; +static uint32_t g_cmd_len; +static uint32_t g_cmd_count; + +static char g_string_pool[STRING_POOL_CAP]; +static uint32_t g_pool_len; + +/* Each string span: offset into pool, length */ +static uint32_t g_spans[MAX_STRINGS][2]; +static uint32_t g_string_count; + +/* --- Helper: add a string to the pool, return its span index --- */ +static uint32_t add_string(const char *s) { + uint32_t len = (uint32_t)strlen(s); + if (g_string_count >= MAX_STRINGS || g_pool_len + len > STRING_POOL_CAP) { + return 0; /* fallback: reuse slot 0 */ + } + uint32_t idx = g_string_count++; + g_spans[idx][0] = g_pool_len; /* offset */ + g_spans[idx][1] = len; /* length */ + memcpy(g_string_pool + g_pool_len, s, len); + g_pool_len += len; + return idx; +} + +/* --- Event buffer for polling --- */ +static uint8_t g_event_buf[8192]; + +/* --- Bridge API (called from ChadScript via `declare function`) --- */ + +/* + * Create engine, enter raw mode. + * Returns opaque engine pointer as i8* (ChadScript "string" type, but + * never dereferenced in TS — just an opaque handle). + */ +char *zr_init(void) { + zr_engine_config_t cfg = zr_engine_config_default(); + cfg.requested_engine_abi_major = ZR_ENGINE_ABI_MAJOR; + cfg.requested_engine_abi_minor = ZR_ENGINE_ABI_MINOR; + cfg.requested_engine_abi_patch = ZR_ENGINE_ABI_PATCH; + cfg.requested_drawlist_version = ZR_DRAWLIST_VERSION_V1; + cfg.requested_event_batch_version = ZR_EVENT_BATCH_VERSION_V1; + cfg.plat.requested_color_mode = PLAT_COLOR_MODE_RGB; + + zr_engine_t *e = NULL; + zr_result_t rc = engine_create(&e, &cfg); + if (rc != ZR_OK) { + fprintf(stderr, "zireael: engine_create failed: %d\n", (int)rc); + return NULL; + } + return (char *)e; +} + +/* Destroy engine, restore terminal. */ +void zr_destroy(char *engine) { engine_destroy((zr_engine_t *)engine); } + +/* + * Poll for events, return a simple string describing the first interesting + * event. ChadScript doesn't have binary buffer parsing, so we do it in C. + * + * Returns: + * "" — no events (timeout) + * "key:escape" — escape key pressed + * "key:up/down/left/right" — arrow keys + * "key:enter" — enter key + * "key:backspace" — backspace key + * "key:tab" — tab key + * "text:X" — text input (single ASCII char) + * "resize:W:H" — terminal resize + * "tick" — tick event + */ +char *zr_poll(char *engine, double timeout_ms) { + int n = engine_poll_events((zr_engine_t *)engine, (int)timeout_ms, + g_event_buf, (int)sizeof(g_event_buf)); + + /* Allocate return string in GC memory so ChadScript can use it */ + char *result = (char *)GC_malloc_atomic(128); + result[0] = '\0'; + + if (n <= 0) + return result; + + /* Validate batch header */ + if ((uint32_t)n < sizeof(zr_evbatch_header_t)) + return result; + uint32_t magic = le32r(g_event_buf + 0); + uint32_t total_size = le32r(g_event_buf + 8); + if (magic != ZR_EV_MAGIC || total_size > (uint32_t)n) + return result; + + /* Iterate records, return info about the first interesting one */ + uint32_t off = (uint32_t)sizeof(zr_evbatch_header_t); + while (off + (uint32_t)sizeof(zr_ev_record_header_t) <= total_size) { + uint32_t type = le32r(g_event_buf + off + 0); + uint32_t size = le32r(g_event_buf + off + 4); + if (size < (uint32_t)sizeof(zr_ev_record_header_t) || off + size > total_size) + break; + + const uint8_t *payload = + g_event_buf + off + (uint32_t)sizeof(zr_ev_record_header_t); + + if (type == (uint32_t)ZR_EV_KEY) { + uint32_t key = le32r(payload + 0); + uint32_t action = le32r(payload + 8); + /* Only report key-down events */ + if (action == (uint32_t)ZR_KEY_ACTION_DOWN) { + switch (key) { + case ZR_KEY_ESCAPE: + strcpy(result, "key:escape"); + return result; + case ZR_KEY_ENTER: + strcpy(result, "key:enter"); + return result; + case ZR_KEY_TAB: + strcpy(result, "key:tab"); + return result; + case ZR_KEY_BACKSPACE: + strcpy(result, "key:backspace"); + return result; + case ZR_KEY_UP: + strcpy(result, "key:up"); + return result; + case ZR_KEY_DOWN: + strcpy(result, "key:down"); + return result; + case ZR_KEY_LEFT: + strcpy(result, "key:left"); + return result; + case ZR_KEY_RIGHT: + strcpy(result, "key:right"); + return result; + default: + break; + } + } + } else if (type == (uint32_t)ZR_EV_TEXT) { + uint32_t codepoint = le32r(payload + 0); + /* Simple ASCII text input */ + if (codepoint >= 32 && codepoint < 127) { + snprintf(result, 128, "text:%c", (char)codepoint); + return result; + } + } else if (type == (uint32_t)ZR_EV_RESIZE) { + uint32_t cols = le32r(payload + 0); + uint32_t rows = le32r(payload + 4); + snprintf(result, 128, "resize:%u:%u", cols, rows); + return result; + } else if (type == (uint32_t)ZR_EV_TICK) { + strcpy(result, "tick"); + return result; + } + + off += align4(size); + } + + return result; +} + +/* Reset drawlist builder for a new frame. */ +void zr_begin(char *engine) { + (void)engine; + g_cmd_len = 0; + g_cmd_count = 0; + g_pool_len = 0; + g_string_count = 0; +} + +/* Append a CLEAR command (8 bytes). */ +void zr_clear(char *engine) { + (void)engine; + if (g_cmd_len + 8 > CMD_BUF_CAP) + return; + le16w(g_cmd_buf + g_cmd_len + 0, (uint16_t)ZR_DL_OP_CLEAR); + le16w(g_cmd_buf + g_cmd_len + 2, 0); + le32w(g_cmd_buf + g_cmd_len + 4, 8); + g_cmd_len += 8; + g_cmd_count++; +} + +/* Append a FILL_RECT command (40 bytes). Colors are 0x00RRGGBB as doubles. */ +void zr_fill_rect(char *engine, double x, double y, double w, double h, + double fg, double bg) { + (void)engine; + if (g_cmd_len + 40 > CMD_BUF_CAP) + return; + uint8_t *p = g_cmd_buf + g_cmd_len; + le16w(p + 0, (uint16_t)ZR_DL_OP_FILL_RECT); + le16w(p + 2, 0); + le32w(p + 4, 40); + le32w(p + 8, (uint32_t)(int32_t)x); + le32w(p + 12, (uint32_t)(int32_t)y); + le32w(p + 16, (uint32_t)(int32_t)w); + le32w(p + 20, (uint32_t)(int32_t)h); + le32w(p + 24, (uint32_t)fg); /* style.fg */ + le32w(p + 28, (uint32_t)bg); /* style.bg */ + le32w(p + 32, 0); /* style.attrs */ + le32w(p + 36, 0); /* style.reserved */ + g_cmd_len += 40; + g_cmd_count++; +} + +/* Append a DRAW_TEXT command (48 bytes). */ +void zr_draw_text(char *engine, double x, double y, char *text, double fg, + double bg) { + (void)engine; + if (g_cmd_len + 48 > CMD_BUF_CAP) + return; + uint32_t str_idx = add_string(text); + uint32_t str_len = (uint32_t)strlen(text); + + uint8_t *p = g_cmd_buf + g_cmd_len; + le16w(p + 0, (uint16_t)ZR_DL_OP_DRAW_TEXT); + le16w(p + 2, 0); + le32w(p + 4, 48); + le32w(p + 8, (uint32_t)(int32_t)x); /* x */ + le32w(p + 12, (uint32_t)(int32_t)y); /* y */ + le32w(p + 16, str_idx); /* string_index */ + le32w(p + 20, 0); /* byte_off */ + le32w(p + 24, str_len); /* byte_len */ + le32w(p + 28, (uint32_t)fg); /* style.fg */ + le32w(p + 32, (uint32_t)bg); /* style.bg */ + le32w(p + 36, 0); /* style.attrs */ + le32w(p + 40, 0); /* style.reserved */ + le32w(p + 44, 0); /* reserved0 */ + g_cmd_len += 48; + g_cmd_count++; +} + +/* + * Finalize the drawlist, submit it to Zireael, and present. + * Builds the full binary drawlist: + * [64-byte header] [command stream] [string spans] [string pool] + * Returns 0.0 on success. + */ +double zr_present(char *engine) { + zr_engine_t *e = (zr_engine_t *)engine; + +#define DL_HEADER_SIZE 64u +#define DL_MAGIC 0x4C44525Au + + uint32_t cmd_off = DL_HEADER_SIZE; + uint32_t spans_off = align4(cmd_off + g_cmd_len); + uint32_t spans_size = + g_string_count * 2 * sizeof(uint32_t); /* 8 bytes per span */ + uint32_t pool_off = align4(spans_off + spans_size); + uint32_t total_size = align4(pool_off + g_pool_len); + + /* Build the drawlist into a stack buffer */ + uint8_t dl_buf[32768]; + if (total_size > sizeof(dl_buf)) { + return -1.0; + } + + memset(dl_buf, 0, total_size); + + /* Header (64 bytes) */ + le32w(dl_buf + 0, DL_MAGIC); + le32w(dl_buf + 4, ZR_DRAWLIST_VERSION_V1); + le32w(dl_buf + 8, DL_HEADER_SIZE); + le32w(dl_buf + 12, total_size); + le32w(dl_buf + 16, cmd_off); + le32w(dl_buf + 20, g_cmd_len); + le32w(dl_buf + 24, g_cmd_count); + le32w(dl_buf + 28, spans_off); + le32w(dl_buf + 32, g_string_count); + le32w(dl_buf + 36, pool_off); + le32w(dl_buf + 40, g_pool_len); + /* blobs: all zeros (no blobs) */ + + /* Command stream */ + memcpy(dl_buf + cmd_off, g_cmd_buf, g_cmd_len); + + /* String spans */ + for (uint32_t i = 0; i < g_string_count; i++) { + le32w(dl_buf + spans_off + i * 8 + 0, g_spans[i][0]); /* offset */ + le32w(dl_buf + spans_off + i * 8 + 4, g_spans[i][1]); /* length */ + } + + /* String pool */ + memcpy(dl_buf + pool_off, g_string_pool, g_pool_len); + + /* Submit + present */ + zr_result_t rc = engine_submit_drawlist(e, dl_buf, (int)total_size); + if (rc != ZR_OK) { + return (double)rc; + } + + rc = engine_present(e); + return (double)rc; +} From 4ff6a07e4539deadcebaa9ec739675aae9a55f03 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 13:09:02 -0800 Subject: [PATCH 04/10] zero-cost FFI type annotations for declare function --- examples/tui/app.tsx | 19 ++++---- examples/tui/build.sh | 6 +-- examples/tui/zireael-bridge.c | 48 +++++++++---------- src/codegen/expressions/calls.ts | 41 +++++++++++++++- src/codegen/infrastructure/type-inference.ts | 4 ++ src/codegen/infrastructure/type-system.ts | 14 ++++++ .../infrastructure/variable-allocator.ts | 2 +- src/codegen/llvm-generator.ts | 2 +- src/codegen/stdlib/embedded-dts.ts | 2 +- 9 files changed, 97 insertions(+), 41 deletions(-) diff --git a/examples/tui/app.tsx b/examples/tui/app.tsx index a67f8b33..ab83e026 100644 --- a/examples/tui/app.tsx +++ b/examples/tui/app.tsx @@ -7,18 +7,19 @@ // Run: .build/examples/tui/app // FFI: zireael-bridge.c — engine lifecycle -declare function zr_init(): string; -declare function zr_destroy(engine: string): void; +// i8_ptr = opaque C pointer, i32/u32 = zero-cost integer types (no double conversion) +declare function zr_init(): i8_ptr; +declare function zr_destroy(engine: i8_ptr): void; // FFI: zireael-bridge.c — event polling (returns "key:escape", "key:up", etc.) -declare function zr_poll(engine: string, timeout_ms: number): string; +declare function zr_poll(engine: i8_ptr, timeout_ms: i32): i8_ptr; // FFI: zireael-bridge.c — drawlist construction -declare function zr_begin(engine: string): void; -declare function zr_clear(engine: string): void; -declare function zr_fill_rect(engine: string, x: number, y: number, w: number, h: number, fg: number, bg: number): void; -declare function zr_draw_text(engine: string, x: number, y: number, text: string, fg: number, bg: number): void; -declare function zr_present(engine: string): number; +declare function zr_begin(engine: i8_ptr): void; +declare function zr_clear(engine: i8_ptr): void; +declare function zr_fill_rect(engine: i8_ptr, x: i32, y: i32, w: i32, h: i32, fg: u32, bg: u32): void; +declare function zr_draw_text(engine: i8_ptr, x: i32, y: i32, text: i8_ptr, fg: u32, bg: u32): void; +declare function zr_present(engine: i8_ptr): f64; // Colors (0x00RRGGBB) const WHITE = 0xFFFFFF; @@ -35,7 +36,7 @@ let running = true; while (running) { const event = zr_poll(engine, 16); - console.log(event); + // console.log(event); if (event === "key:escape") { running = false; } else if (event === "key:up") { diff --git a/examples/tui/build.sh b/examples/tui/build.sh index d2fd0108..ce53b379 100755 --- a/examples/tui/build.sh +++ b/examples/tui/build.sh @@ -34,11 +34,9 @@ clang -c -O2 -fPIC \ echo "Building app.tsx..." cd "$REPO_DIR" -# Use node compiler (dist/chad-node.js) if native chad isn't available +# Use node compiler — the native compiler has a known codegen bug with +# declare function (Heisenbug in generateDeclareFunction). Use node for now. CHAD="node dist/chad-node.js" -if [ -f ".build/chad" ]; then - CHAD=".build/chad" -fi $CHAD build examples/tui/app.tsx \ -o .build/examples/tui/app \ diff --git a/examples/tui/zireael-bridge.c b/examples/tui/zireael-bridge.c index e63abbcd..b6c07083 100644 --- a/examples/tui/zireael-bridge.c +++ b/examples/tui/zireael-bridge.c @@ -124,7 +124,7 @@ void zr_destroy(char *engine) { engine_destroy((zr_engine_t *)engine); } * "resize:W:H" — terminal resize * "tick" — tick event */ -char *zr_poll(char *engine, double timeout_ms) { +char *zr_poll(char *engine, int32_t timeout_ms) { int n = engine_poll_events((zr_engine_t *)engine, (int)timeout_ms, g_event_buf, (int)sizeof(g_event_buf)); @@ -232,9 +232,9 @@ void zr_clear(char *engine) { g_cmd_count++; } -/* Append a FILL_RECT command (40 bytes). Colors are 0x00RRGGBB as doubles. */ -void zr_fill_rect(char *engine, double x, double y, double w, double h, - double fg, double bg) { +/* Append a FILL_RECT command (40 bytes). Zero-cost FFI: types match LLVM IR. */ +void zr_fill_rect(char *engine, int32_t x, int32_t y, int32_t w, int32_t h, + uint32_t fg, uint32_t bg) { (void)engine; if (g_cmd_len + 40 > CMD_BUF_CAP) return; @@ -242,21 +242,21 @@ void zr_fill_rect(char *engine, double x, double y, double w, double h, le16w(p + 0, (uint16_t)ZR_DL_OP_FILL_RECT); le16w(p + 2, 0); le32w(p + 4, 40); - le32w(p + 8, (uint32_t)(int32_t)x); - le32w(p + 12, (uint32_t)(int32_t)y); - le32w(p + 16, (uint32_t)(int32_t)w); - le32w(p + 20, (uint32_t)(int32_t)h); - le32w(p + 24, (uint32_t)fg); /* style.fg */ - le32w(p + 28, (uint32_t)bg); /* style.bg */ - le32w(p + 32, 0); /* style.attrs */ - le32w(p + 36, 0); /* style.reserved */ + le32w(p + 8, (uint32_t)x); + le32w(p + 12, (uint32_t)y); + le32w(p + 16, (uint32_t)w); + le32w(p + 20, (uint32_t)h); + le32w(p + 24, fg); /* style.fg */ + le32w(p + 28, bg); /* style.bg */ + le32w(p + 32, 0); /* style.attrs */ + le32w(p + 36, 0); /* style.reserved */ g_cmd_len += 40; g_cmd_count++; } /* Append a DRAW_TEXT command (48 bytes). */ -void zr_draw_text(char *engine, double x, double y, char *text, double fg, - double bg) { +void zr_draw_text(char *engine, int32_t x, int32_t y, char *text, uint32_t fg, + uint32_t bg) { (void)engine; if (g_cmd_len + 48 > CMD_BUF_CAP) return; @@ -267,16 +267,16 @@ void zr_draw_text(char *engine, double x, double y, char *text, double fg, le16w(p + 0, (uint16_t)ZR_DL_OP_DRAW_TEXT); le16w(p + 2, 0); le32w(p + 4, 48); - le32w(p + 8, (uint32_t)(int32_t)x); /* x */ - le32w(p + 12, (uint32_t)(int32_t)y); /* y */ - le32w(p + 16, str_idx); /* string_index */ - le32w(p + 20, 0); /* byte_off */ - le32w(p + 24, str_len); /* byte_len */ - le32w(p + 28, (uint32_t)fg); /* style.fg */ - le32w(p + 32, (uint32_t)bg); /* style.bg */ - le32w(p + 36, 0); /* style.attrs */ - le32w(p + 40, 0); /* style.reserved */ - le32w(p + 44, 0); /* reserved0 */ + le32w(p + 8, (uint32_t)x); /* x */ + le32w(p + 12, (uint32_t)y); /* y */ + le32w(p + 16, str_idx); /* string_index */ + le32w(p + 20, 0); /* byte_off */ + le32w(p + 24, str_len); /* byte_len */ + le32w(p + 28, fg); /* style.fg */ + le32w(p + 32, bg); /* style.bg */ + le32w(p + 36, 0); /* style.attrs */ + le32w(p + 40, 0); /* style.reserved */ + le32w(p + 44, 0); /* reserved0 */ g_cmd_len += 48; g_cmd_count++; } diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index c93fa519..6f1087cf 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -617,12 +617,33 @@ export class CallExpressionGenerator { } else if (paramType === "double" && resultType === "i64") { const coerced = this.ctx.ensureDouble(result); argsList.push(`double ${coerced}`); + } else if (paramType === "i32" && (resultType === "double" || !resultType)) { + // FFI: double → i32 (e.g., number literal passed to C int32_t param) + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = fptosi double ${result} to i32`); + argsList.push(`i32 ${coerced}`); + } else if (paramType === "i32" && resultType === "i64") { + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = trunc i64 ${result} to i32`); + argsList.push(`i32 ${coerced}`); + } else if (paramType === "i64" && (resultType === "double" || !resultType)) { + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = fptosi double ${result} to i64`); + argsList.push(`i64 ${coerced}`); + } else if (paramType === "float" && (resultType === "double" || !resultType)) { + // FFI: double → float (e.g., number literal passed to C float param) + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = fptrunc double ${result} to float`); + argsList.push(`float ${coerced}`); } else { argsList.push(`${paramType} ${result}`); } } else { const paramType = paramTypes[i] || "double"; - const defaultVal = paramType === "double" ? "0.0" : "null"; + let defaultVal = "null"; + if (paramType === "double") defaultVal = "0.0"; + else if (paramType === "float") defaultVal = "0.0"; + else if (paramType === "i32" || paramType === "i64" || paramType === "i16" || paramType === "i8") defaultVal = "0"; argsList.push(`${paramType} ${defaultVal}`); } } @@ -639,6 +660,24 @@ export class CallExpressionGenerator { const temp = this.ctx.emitCall(returnType, `@${mangledName}`, argsList.join(", ")); + // FFI return type coercion: convert non-standard LLVM types back to + // ChadScript's type system (double for numbers, i8* for pointers) + if (returnType === "i32" || returnType === "i16" || returnType === "i8") { + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = sitofp ${returnType} ${temp} to double`); + return coerced; + } + if (returnType === "i64") { + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = sitofp i64 ${temp} to double`); + return coerced; + } + if (returnType === "float") { + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = fpext float ${temp} to double`); + return coerced; + } + return temp; } diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 2a81bfbc..12b836b5 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -36,6 +36,8 @@ function isStringType(t: string): boolean { if (t === "string") return true; if (t === "string | null" || t === "string | undefined") return true; if (t === "null | string" || t === "undefined | string") return true; + // FFI pointer types (i8_ptr, ptr) are stored as i8* like strings + if (t === "i8_ptr" || t === "ptr") return true; return false; } @@ -2336,6 +2338,8 @@ export class TypeInference { private returnTypeIsString(returnType: string): boolean { if (returnType === "string") return true; + // FFI pointer types map to i8* like strings + if (returnType === "i8_ptr" || returnType === "ptr") return true; if (returnType.indexOf(" | ") !== -1) { const parts = returnType.split(" | "); for (let i = 0; i < parts.length; i++) { diff --git a/src/codegen/infrastructure/type-system.ts b/src/codegen/infrastructure/type-system.ts index 6a61f4b6..87090f6e 100644 --- a/src/codegen/infrastructure/type-system.ts +++ b/src/codegen/infrastructure/type-system.ts @@ -160,6 +160,20 @@ export function canonicalTypeToLlvm( return "i8*"; } + // FFI type passthrough — zero-cost: maps directly to LLVM types with no + // double conversion. Used in `declare function` for calling C code. + if (tsType === "i8") return "i8"; + if (tsType === "i16") return "i16"; + if (tsType === "i32") return "i32"; + if (tsType === "i64") return "i64"; + if (tsType === "u8") return "i8"; + if (tsType === "u16") return "i16"; + if (tsType === "u32") return "i32"; + if (tsType === "u64") return "i64"; + if (tsType === "f32") return "float"; + if (tsType === "f64") return "double"; + if (tsType === "i8_ptr" || tsType === "ptr") return "i8*"; + if (fieldName === "nodePtr" || fieldName === "treePtr") return "i8*"; if (mode === "param") { diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index e01320e7..ee34c0b4 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -485,7 +485,7 @@ export class VariableAllocator { if (stmt.value === null || isAstNullLiteral) { const allocaReg = this.ctx.nextAllocaReg(stmt.name); const baseType = stmt.declaredType ? stripNullable(stmt.declaredType) : ""; - if (baseType === "string") { + if (baseType === "string" || baseType === "i8_ptr" || baseType === "ptr") { this.ctx.defineVariable(stmt.name, allocaReg, "i8*", SymbolKind.String, "local"); this.ctx.emit(`${allocaReg} = alloca i8*`); this.ctx.emit(`store i8* null, i8** ${allocaReg}`); diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index d3d512da..549b4de0 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -1810,7 +1810,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { if (!fn) continue; if (fn.name === callNode.name && fn.returnType) { const rt = fn.returnType; - if (rt === "string") { + if (rt === "string" || rt === "i8_ptr" || rt === "ptr") { ir += `@${name} = global i8* null` + "\n"; this.globalVariables.set(name, { llvmType: "i8*", diff --git a/src/codegen/stdlib/embedded-dts.ts b/src/codegen/stdlib/embedded-dts.ts index 09f03749..08d78f5b 100644 --- a/src/codegen/stdlib/embedded-dts.ts +++ b/src/codegen/stdlib/embedded-dts.ts @@ -1,3 +1,3 @@ export function getDtsContent(): string { - return '/**\n * ChadScript Built-in Global Types\n *\n * Type definitions for ChadScript\'s built-in runtime APIs. These globals\n * are available without imports in all ChadScript programs and are compiled\n * directly to native code via LLVM.\n *\n * Generate this file in any project with: chad init\n *\n * Note: Standard JavaScript types (String, Number, Array, etc.) are provided\n * by TypeScript\'s ES2020 lib. This file only defines ChadScript-specific APIs.\n */\n\n// ============================================================================\n// Console\n// ============================================================================\n\ndeclare namespace console {\n function log(...args: any[]): void;\n function error(...args: any[]): void;\n function warn(...args: any[]): void;\n function debug(...args: any[]): void;\n}\n\n// ============================================================================\n// Process\n// ============================================================================\n\ndeclare namespace process {\n const argv: string[];\n const argv0: string;\n const platform: string;\n const arch: string;\n const pid: number;\n const ppid: number;\n const execPath: string;\n const version: string;\n\n const env: { [key: string]: string };\n\n function exit(code?: number): never;\n function cwd(): string;\n function chdir(path: string): void;\n function uptime(): number;\n function kill(pid: number, signal?: number): void;\n function abort(): never;\n function getuid(): number;\n function getgid(): number;\n function geteuid(): number;\n function getegid(): number;\n\n namespace stdout {\n function write(str: string): void;\n }\n namespace stderr {\n function write(str: string): void;\n }\n}\n\n// ============================================================================\n// Filesystem\n// ============================================================================\n\ndeclare namespace fs {\n function readFileSync(filename: string): string;\n function writeFileSync(filename: string, data: string): number;\n function appendFileSync(filename: string, data: string): void;\n function existsSync(filename: string): boolean;\n function unlinkSync(filename: string): number;\n function readdirSync(path: string): string[];\n function statSync(path: string): { size: number; isFile(): boolean; isDirectory(): boolean };\n}\n\n// ============================================================================\n// Path\n// ============================================================================\n\ndeclare namespace path {\n function join(a: string, b: string): string;\n function resolve(p: string): string;\n function dirname(p: string): string;\n function basename(p: string): string;\n}\n\n// ============================================================================\n// Math\n// ============================================================================\n\ndeclare namespace Math {\n const PI: number;\n const E: number;\n\n function sqrt(x: number): number;\n function pow(base: number, exp: number): number;\n function floor(x: number): number;\n function ceil(x: number): number;\n function round(x: number): number;\n function abs(x: number): number;\n function max(a: number, b: number): number;\n function min(a: number, b: number): number;\n function random(): number;\n function log(x: number): number;\n function log2(x: number): number;\n function log10(x: number): number;\n function sin(x: number): number;\n function cos(x: number): number;\n function tan(x: number): number;\n function trunc(x: number): number;\n function sign(x: number): number;\n}\n\n// ============================================================================\n// Date\n// ============================================================================\n\ndeclare namespace Date {\n function now(): number;\n}\n\n// ============================================================================\n// JSON\n// ============================================================================\n\ndeclare namespace JSON {\n function parse(str: string): T;\n function stringify(value: any): string;\n}\n\n// ============================================================================\n// Crypto\n// ============================================================================\n\ndeclare namespace crypto {\n function sha256(input: string): string;\n function sha512(input: string): string;\n function md5(input: string): string;\n function randomBytes(n: number): string;\n}\n\n// ============================================================================\n// SQLite\n// ============================================================================\n\ndeclare namespace sqlite {\n function open(path: string): any;\n function exec(db: any, sql: string): void;\n function get(db: any, sql: string): string;\n function all(db: any, sql: string): string[];\n function close(db: any): void;\n}\n\n// ============================================================================\n// Child Process\n// ============================================================================\n\ndeclare namespace child_process {\n function execSync(command: string): string;\n function spawnSync(\n command: string,\n args?: string[],\n ): { stdout: string; stderr: string; status: number };\n}\n\n// ============================================================================\n// OS\n// ============================================================================\n\ndeclare namespace os {\n const platform: string;\n const arch: string;\n const EOL: string;\n\n function hostname(): string;\n function homedir(): string;\n function tmpdir(): string;\n function cpus(): number;\n function totalmem(): number;\n function freemem(): number;\n function uptime(): number;\n}\n\n// ============================================================================\n// TTY\n// ============================================================================\n\ndeclare namespace tty {\n function isatty(fd: number): boolean;\n}\n\n// ============================================================================\n// Number\n// ============================================================================\n\ndeclare namespace Number {\n function isFinite(x: number): boolean;\n function isNaN(x: number): boolean;\n function isInteger(x: number): boolean;\n}\n\n// ============================================================================\n// Object\n// ============================================================================\n\ndeclare namespace Object {\n function keys(obj: any): string[];\n function values(obj: any): string[];\n function entries(obj: any): string[];\n}\n\n// ============================================================================\n// HTTP & Networking\n// ============================================================================\n\ninterface Response {\n text(): string;\n json(): T;\n status: number;\n ok: boolean;\n}\n\ndeclare function fetch(url: string): Promise;\n\ninterface HttpRequest {\n method: string;\n path: string;\n body: string;\n contentType: string;\n}\n\ninterface HttpResponse {\n status: number;\n body: string;\n}\n\ndeclare function httpServe(port: number, handler: (req: HttpRequest) => HttpResponse): void;\n\n// ============================================================================\n// Async / Timers\n// ============================================================================\n\ndeclare function setTimeout(callback: () => void, delay: number): number;\ndeclare function setInterval(callback: () => void, interval: number): number;\ndeclare function clearTimeout(id: number): void;\ndeclare function clearInterval(id: number): void;\ndeclare function runEventLoop(): void;\n\n// ============================================================================\n// Global Functions\n// ============================================================================\n\ndeclare function parseInt(str: string, radix?: number): number;\ndeclare function parseFloat(str: string): number;\ndeclare function isNaN(value: any): boolean;\ndeclare function execSync(command: string): string;\n\n// ============================================================================\n// Low-Level System Calls\n// ============================================================================\n\ndeclare function malloc(size: number): number;\ndeclare function free(ptr: number): void;\ndeclare function socket(domain: number, type: number, protocol: number): number;\ndeclare function bind(socket: number, addr: number, addrlen: number): number;\ndeclare function listen(socket: number, backlog: number): number;\ndeclare function accept(socket: number, addr: number, addrlen: number): number;\ndeclare function htons(hostshort: number): number;\ndeclare function close(fd: number): number;\ndeclare function read(fd: number, buf: number, count: number): number;\ndeclare function write(fd: number, buf: number, count: number): number;\n\n// ============================================================================\n// Test Runner\n// ============================================================================\n\ndeclare namespace assert {\n function strictEqual(actual: any, expected: any): void;\n function notStrictEqual(actual: any, expected: any): void;\n function deepEqual(actual: any, expected: any): void;\n function ok(value: any): void;\n function fail(message?: string): void;\n}\n\ndeclare function test(name: string, fn: () => void): void;\ndeclare function describe(name: string, fn: () => void): void;\n\n// ============================================================================\n// Compile-Time File Embedding\n// ============================================================================\n\ndeclare namespace ChadScript {\n function embedFile(path: string): string;\n function embedDir(path: string): void;\n function getEmbeddedFile(key: string): string;\n}\n'; + return '/**\n * ChadScript Built-in Global Types\n *\n * Type definitions for ChadScript\'s built-in runtime APIs. These globals\n * are available without imports in all ChadScript programs and are compiled\n * directly to native code via LLVM.\n *\n * Generate this file in any project with: chad init\n *\n * Note: Standard JavaScript types (String, Number, Array, etc.) are provided\n * by TypeScript\'s ES2020 lib. This file only defines ChadScript-specific APIs.\n */\n\n// ============================================================================\n// Console\n// ============================================================================\n\ndeclare namespace console {\n function log(...args: any[]): void;\n function error(...args: any[]): void;\n function warn(...args: any[]): void;\n function debug(...args: any[]): void;\n}\n\n// ============================================================================\n// Process\n// ============================================================================\n\ndeclare namespace process {\n const argv: string[];\n const argv0: string;\n const platform: string;\n const arch: string;\n const pid: number;\n const ppid: number;\n const execPath: string;\n const version: string;\n\n const env: { [key: string]: string };\n\n function exit(code?: number): never;\n function cwd(): string;\n function chdir(path: string): void;\n function uptime(): number;\n function kill(pid: number, signal?: number): void;\n function abort(): never;\n function getuid(): number;\n function getgid(): number;\n function geteuid(): number;\n function getegid(): number;\n\n namespace stdout {\n function write(str: string): void;\n }\n namespace stderr {\n function write(str: string): void;\n }\n}\n\n// ============================================================================\n// Filesystem\n// ============================================================================\n\ndeclare namespace fs {\n function readFileSync(filename: string): string;\n function writeFileSync(filename: string, data: string): number;\n function appendFileSync(filename: string, data: string): void;\n function existsSync(filename: string): boolean;\n function unlinkSync(filename: string): number;\n function readdirSync(path: string): string[];\n function statSync(path: string): { size: number; isFile(): boolean; isDirectory(): boolean };\n}\n\n// ============================================================================\n// Path\n// ============================================================================\n\ndeclare namespace path {\n function join(a: string, b: string): string;\n function resolve(p: string): string;\n function dirname(p: string): string;\n function basename(p: string): string;\n}\n\n// ============================================================================\n// Math\n// ============================================================================\n\ndeclare namespace Math {\n const PI: number;\n const E: number;\n\n function sqrt(x: number): number;\n function pow(base: number, exp: number): number;\n function floor(x: number): number;\n function ceil(x: number): number;\n function round(x: number): number;\n function abs(x: number): number;\n function max(a: number, b: number): number;\n function min(a: number, b: number): number;\n function random(): number;\n function log(x: number): number;\n function log2(x: number): number;\n function log10(x: number): number;\n function sin(x: number): number;\n function cos(x: number): number;\n function tan(x: number): number;\n function trunc(x: number): number;\n function sign(x: number): number;\n}\n\n// ============================================================================\n// Date\n// ============================================================================\n\ndeclare namespace Date {\n function now(): number;\n}\n\n// ============================================================================\n// JSON\n// ============================================================================\n\ndeclare namespace JSON {\n function parse(str: string): T;\n function stringify(value: any): string;\n}\n\n// ============================================================================\n// Crypto\n// ============================================================================\n\ndeclare namespace crypto {\n function sha256(input: string): string;\n function sha512(input: string): string;\n function md5(input: string): string;\n function randomBytes(n: number): string;\n}\n\n// ============================================================================\n// SQLite\n// ============================================================================\n\ndeclare namespace sqlite {\n function open(path: string): any;\n function exec(db: any, sql: string): void;\n function get(db: any, sql: string): string;\n function all(db: any, sql: string): string[];\n function close(db: any): void;\n}\n\n// ============================================================================\n// Child Process\n// ============================================================================\n\ndeclare namespace child_process {\n function execSync(command: string): string;\n function spawnSync(\n command: string,\n args?: string[],\n ): { stdout: string; stderr: string; status: number };\n}\n\n// ============================================================================\n// OS\n// ============================================================================\n\ndeclare namespace os {\n const platform: string;\n const arch: string;\n const EOL: string;\n\n function hostname(): string;\n function homedir(): string;\n function tmpdir(): string;\n function cpus(): number;\n function totalmem(): number;\n function freemem(): number;\n function uptime(): number;\n}\n\n// ============================================================================\n// TTY\n// ============================================================================\n\ndeclare namespace tty {\n function isatty(fd: number): boolean;\n}\n\n// ============================================================================\n// Number\n// ============================================================================\n\ndeclare namespace Number {\n function isFinite(x: number): boolean;\n function isNaN(x: number): boolean;\n function isInteger(x: number): boolean;\n}\n\n// ============================================================================\n// Object\n// ============================================================================\n\ndeclare namespace Object {\n function keys(obj: any): string[];\n function values(obj: any): string[];\n function entries(obj: any): string[];\n}\n\n// ============================================================================\n// HTTP & Networking\n// ============================================================================\n\ninterface Response {\n text(): string;\n json(): T;\n status: number;\n ok: boolean;\n}\n\ndeclare function fetch(url: string): Promise;\n\ninterface HttpRequest {\n method: string;\n path: string;\n body: string;\n contentType: string;\n}\n\ninterface HttpResponse {\n status: number;\n body: string;\n}\n\ndeclare function httpServe(port: number, handler: (req: HttpRequest) => HttpResponse): void;\n\n// ============================================================================\n// Async / Timers\n// ============================================================================\n\ndeclare function setTimeout(callback: () => void, delay: number): number;\ndeclare function setInterval(callback: () => void, interval: number): number;\ndeclare function clearTimeout(id: number): void;\ndeclare function clearInterval(id: number): void;\ndeclare function runEventLoop(): void;\n\n// ============================================================================\n// Global Functions\n// ============================================================================\n\ndeclare function parseInt(str: string, radix?: number): number;\ndeclare function parseFloat(str: string): number;\ndeclare function isNaN(value: any): boolean;\ndeclare function execSync(command: string): string;\n\n// ============================================================================\n// Low-Level System Calls\n// ============================================================================\n\ndeclare function malloc(size: number): number;\ndeclare function free(ptr: number): void;\ndeclare function socket(domain: number, type: number, protocol: number): number;\ndeclare function bind(socket: number, addr: number, addrlen: number): number;\ndeclare function listen(socket: number, backlog: number): number;\ndeclare function accept(socket: number, addr: number, addrlen: number): number;\ndeclare function htons(hostshort: number): number;\ndeclare function close(fd: number): number;\ndeclare function read(fd: number, buf: number, count: number): number;\ndeclare function write(fd: number, buf: number, count: number): number;\n\n// ============================================================================\n// Test Runner\n// ============================================================================\n\ndeclare namespace assert {\n function strictEqual(actual: any, expected: any): void;\n function notStrictEqual(actual: any, expected: any): void;\n function deepEqual(actual: any, expected: any): void;\n function ok(value: any): void;\n function fail(message?: string): void;\n}\n\ndeclare function test(name: string, fn: () => void): void;\ndeclare function describe(name: string, fn: () => void): void;\n\n// ============================================================================\n// Compile-Time File Embedding\n// ============================================================================\n\ndeclare namespace ChadScript {\n function embedFile(path: string): string;\n function embedDir(path: string): void;\n function getEmbeddedFile(key: string): string;\n}\n\n// ============================================================================\n// FFI Type Aliases — zero-cost native types for declare function\n// ============================================================================\n\n// Integer types (map directly to LLVM integer types, no double conversion)\ntype i8 = number;\ntype i16 = number;\ntype i32 = number;\ntype i64 = number;\ntype u8 = number;\ntype u16 = number;\ntype u32 = number;\ntype u64 = number;\n\n// Floating-point types\ntype f32 = number;\ntype f64 = number;\n\n// Pointer types (map to LLVM i8*, used as opaque handles)\ntype i8_ptr = string;\ntype ptr = string;\n'; } From 6e0fa475cc66f81b684ba23d7774eb9820a8c901 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 13:24:10 -0800 Subject: [PATCH 05/10] fix closure capture struct layout after adding declare field to FunctionNode --- src/codegen/expressions/arrow-functions.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/codegen/expressions/arrow-functions.ts b/src/codegen/expressions/arrow-functions.ts index d75bb96c..32071385 100644 --- a/src/codegen/expressions/arrow-functions.ts +++ b/src/codegen/expressions/arrow-functions.ts @@ -155,12 +155,19 @@ export class ArrowFunctionExpressionGenerator extends BaseGenerator { } as BlockStatement; } + // All FunctionNode fields must be present so the native compiler allocates + // the full struct size — closureInfo is the 11th field (after declare). const liftedFunc: LiftedFunction = { name: funcName, params: funcParams, body: liftedBody, returnType: liftedReturnType, paramTypes: liftedParamTypes, + typeParameters: undefined, + async: undefined, + parameters: undefined, + loc: undefined, + declare: false, closureInfo, }; @@ -210,9 +217,9 @@ export class ArrowFunctionExpressionGenerator extends BaseGenerator { } if (funcResult) { // Type assertion must include ALL fields from FunctionNode + closureInfo - // in exact struct order. LiftedFunction extends FunctionNode (9 fields), - // so closureInfo is at index 9, not index 5. Omitting the middle fields - // causes GEP to read the wrong offset (e.g., typeParameters instead of closureInfo). + // in exact struct order. LiftedFunction extends FunctionNode (10 fields), + // so closureInfo is at index 10. Omitting middle fields causes GEP to + // read the wrong offset in native code. const func = funcResult as { name: string; params: string[]; @@ -223,6 +230,7 @@ export class ArrowFunctionExpressionGenerator extends BaseGenerator { async: boolean; parameters: FunctionParameter[]; loc: SourceLocation; + declare: boolean; closureInfo: ClosureInfo; }; return func.closureInfo; From f7b94835d13c3e152bd62ec6442af57a16d97700 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 13:41:14 -0800 Subject: [PATCH 06/10] add jsx tui example alongside imperative version --- examples/tui/app-jsx.tsx | 98 ++++++++++++++++++++++++++++++++++++++++ examples/tui/build.sh | 21 ++++++--- 2 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 examples/tui/app-jsx.tsx diff --git a/examples/tui/app-jsx.tsx b/examples/tui/app-jsx.tsx new file mode 100644 index 00000000..94347aa5 --- /dev/null +++ b/examples/tui/app-jsx.tsx @@ -0,0 +1,98 @@ +// ChadScript JSX TUI Demo — declarative version of the Zireael counter app +// +// Same interactive counter as app.tsx, but uses JSX syntax for the draw calls. +// JSX desugars to createElement("Tag", {prop: v}, []). +// Our createElement draws directly to the terminal as a side effect. +// +// Build: bash examples/tui/build.sh +// Run: .build/examples/tui/app-jsx + +// FFI: zireael-bridge.c — engine lifecycle +declare function zr_init(): i8_ptr; +declare function zr_destroy(engine: i8_ptr): void; + +// FFI: zireael-bridge.c — event polling (returns "key:escape", "key:up", etc.) +declare function zr_poll(engine: i8_ptr, timeout_ms: i32): i8_ptr; + +// FFI: zireael-bridge.c — drawlist construction +declare function zr_begin(engine: i8_ptr): void; +declare function zr_clear(engine: i8_ptr): void; +declare function zr_fill_rect(engine: i8_ptr, x: i32, y: i32, w: i32, h: i32, fg: u32, bg: u32): void; +declare function zr_draw_text(engine: i8_ptr, x: i32, y: i32, text: i8_ptr, fg: u32, bg: u32): void; +declare function zr_present(engine: i8_ptr): f64; + +// Colors (0x00RRGGBB) +const WHITE = 0xFFFFFF; +const CYAN = 0x00FFFF; +const GRAY = 0x888888; +const DARK_BLUE = 0x002244; +const BLACK = 0x000000; +const GREEN = 0x00FF88; + +// Props for all JSX elements — every element receives the full set. +// Box uses x/y/w/h/fg/bg for fill_rect; Text uses x/y/text/fg/bg for draw_text. +interface Props { + x: number; + y: number; + w: number; + h: number; + fg: number; + bg: number; + text: string; +} + +// Module-level engine so createElement can access it +const engine = zr_init(); + +// Side-effect createElement: draws to the terminal immediately via Zireael FFI. +// JSX evaluates children before parents (inner-to-outer), so we use flat fragments +// (<>...) instead of nesting — this gives us left-to-right draw order, ensuring +// backgrounds (Box) render before foreground text (Text). +function createElement(tag: string, props: Props, children: string[]): string { + if (tag === "Box") { + zr_fill_rect(engine, props.x, props.y, props.w, props.h, props.fg, props.bg); + } else if (tag === "Text") { + zr_draw_text(engine, props.x, props.y, props.text, props.fg, props.bg); + } + // "Fragment" (from <>) does nothing — just groups siblings for ordering + return tag; +} + +let count = 0; +let running = true; + +while (running) { + const event = zr_poll(engine, 16); + + if (event === "key:escape") { + running = false; + } else if (event === "key:up") { + count = count + 1; + } else if (event === "key:down") { + count = count - 1; + } else if (event === "key:right") { + count = count + 10; + } else if (event === "key:left") { + count = count - 10; + } + + zr_begin(engine); + zr_clear(engine); + + // JSX renders via side effects — each element triggers zr_fill_rect or zr_draw_text. + // Fragment (<>) ensures left-to-right evaluation: Box backgrounds draw first, then Text. + const ui = ( + <> + + + + + + + + ); + + zr_present(engine); +} + +zr_destroy(engine); diff --git a/examples/tui/build.sh b/examples/tui/build.sh index ce53b379..05a687b1 100755 --- a/examples/tui/build.sh +++ b/examples/tui/build.sh @@ -1,11 +1,12 @@ #!/bin/bash -# Build the ChadScript TUI demo (Zireael-powered terminal app). +# Build the ChadScript TUI demos (Zireael-powered terminal apps). # # This uses `chad build` with --link-obj/--link-path/--link-lib to compile -# the TypeScript app, link the C bridge, and produce a native binary. +# the TypeScript apps, link the C bridge, and produce native binaries. # # Usage: bash examples/tui/build.sh -# Run: .build/examples/tui/app +# Run: .build/examples/tui/app (imperative) +# .build/examples/tui/app-jsx (JSX) set -euo pipefail @@ -30,19 +31,27 @@ clang -c -O2 -fPIC \ "$BRIDGE_SRC" \ -o "$BRIDGE_OBJ" -# --- Step 3: Build the app with chad --- -echo "Building app.tsx..." +# --- Step 3: Build the apps with chad --- cd "$REPO_DIR" # Use node compiler — the native compiler has a known codegen bug with # declare function (Heisenbug in generateDeclareFunction). Use node for now. CHAD="node dist/chad-node.js" +echo "Building app.tsx..." $CHAD build examples/tui/app.tsx \ -o .build/examples/tui/app \ --link-obj "$BRIDGE_OBJ,$ZIREAEL_LIB" \ --link-lib pthread +echo "Building app-jsx.tsx..." +$CHAD build examples/tui/app-jsx.tsx \ + -o .build/examples/tui/app-jsx \ + --link-obj "$BRIDGE_OBJ,$ZIREAEL_LIB" \ + --link-lib pthread + echo "" -echo "Built: .build/examples/tui/app" +echo "Built: .build/examples/tui/app (imperative)" +echo " .build/examples/tui/app-jsx (JSX)" echo "Run: .build/examples/tui/app" +echo " .build/examples/tui/app-jsx" From 8579174baf337c41228b2bbb06d2825aa03b2a8f Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 13:47:03 -0800 Subject: [PATCH 07/10] prettier formatting --- examples/tui/app-jsx.tsx | 16 ++++++++++++---- examples/tui/app.tsx | 18 +++++++++++++----- src/codegen/expressions/calls.ts | 8 +++++++- tests/fixtures/jsx/jsx-nested.tsx | 6 +++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/examples/tui/app-jsx.tsx b/examples/tui/app-jsx.tsx index 94347aa5..bc0a2312 100644 --- a/examples/tui/app-jsx.tsx +++ b/examples/tui/app-jsx.tsx @@ -17,17 +17,25 @@ declare function zr_poll(engine: i8_ptr, timeout_ms: i32): i8_ptr; // FFI: zireael-bridge.c — drawlist construction declare function zr_begin(engine: i8_ptr): void; declare function zr_clear(engine: i8_ptr): void; -declare function zr_fill_rect(engine: i8_ptr, x: i32, y: i32, w: i32, h: i32, fg: u32, bg: u32): void; +declare function zr_fill_rect( + engine: i8_ptr, + x: i32, + y: i32, + w: i32, + h: i32, + fg: u32, + bg: u32, +): void; declare function zr_draw_text(engine: i8_ptr, x: i32, y: i32, text: i8_ptr, fg: u32, bg: u32): void; declare function zr_present(engine: i8_ptr): f64; // Colors (0x00RRGGBB) -const WHITE = 0xFFFFFF; -const CYAN = 0x00FFFF; +const WHITE = 0xffffff; +const CYAN = 0x00ffff; const GRAY = 0x888888; const DARK_BLUE = 0x002244; const BLACK = 0x000000; -const GREEN = 0x00FF88; +const GREEN = 0x00ff88; // Props for all JSX elements — every element receives the full set. // Box uses x/y/w/h/fg/bg for fill_rect; Text uses x/y/text/fg/bg for draw_text. diff --git a/examples/tui/app.tsx b/examples/tui/app.tsx index ab83e026..40702916 100644 --- a/examples/tui/app.tsx +++ b/examples/tui/app.tsx @@ -17,18 +17,26 @@ declare function zr_poll(engine: i8_ptr, timeout_ms: i32): i8_ptr; // FFI: zireael-bridge.c — drawlist construction declare function zr_begin(engine: i8_ptr): void; declare function zr_clear(engine: i8_ptr): void; -declare function zr_fill_rect(engine: i8_ptr, x: i32, y: i32, w: i32, h: i32, fg: u32, bg: u32): void; +declare function zr_fill_rect( + engine: i8_ptr, + x: i32, + y: i32, + w: i32, + h: i32, + fg: u32, + bg: u32, +): void; declare function zr_draw_text(engine: i8_ptr, x: i32, y: i32, text: i8_ptr, fg: u32, bg: u32): void; declare function zr_present(engine: i8_ptr): f64; // Colors (0x00RRGGBB) -const WHITE = 0xFFFFFF; -const CYAN = 0x00FFFF; +const WHITE = 0xffffff; +const CYAN = 0x00ffff; const GRAY = 0x888888; const DARK_BLUE = 0x002244; const BLACK = 0x000000; -const GREEN = 0x00FF88; -const YELLOW = 0xFFCC00; +const GREEN = 0x00ff88; +const YELLOW = 0xffcc00; const engine = zr_init(); let count = 0; diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index 6f1087cf..f4a9efa6 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -643,7 +643,13 @@ export class CallExpressionGenerator { let defaultVal = "null"; if (paramType === "double") defaultVal = "0.0"; else if (paramType === "float") defaultVal = "0.0"; - else if (paramType === "i32" || paramType === "i64" || paramType === "i16" || paramType === "i8") defaultVal = "0"; + else if ( + paramType === "i32" || + paramType === "i64" || + paramType === "i16" || + paramType === "i8" + ) + defaultVal = "0"; argsList.push(`${paramType} ${defaultVal}`); } } diff --git a/tests/fixtures/jsx/jsx-nested.tsx b/tests/fixtures/jsx/jsx-nested.tsx index 910e5871..bfb56bb3 100644 --- a/tests/fixtures/jsx/jsx-nested.tsx +++ b/tests/fixtures/jsx/jsx-nested.tsx @@ -11,7 +11,11 @@ function createElement(tag: string, props: EmptyProps, children: string[]): stri return result; } -const result = hello; +const result = ( + + hello + +); if (result === "Box(Text(hello))") { console.log("TEST_PASSED"); } From 31d4dce90456f581e6a73c11ac14fc2debca4109 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 18:11:21 -0800 Subject: [PATCH 08/10] fix native compiler segfault from TBAA miscompilation and struct layout mismatch --- src/codegen/expressions/access/index.ts | 22 +++--- src/codegen/expressions/access/member.ts | 55 +++++++-------- .../expressions/access/property-handlers.ts | 6 +- src/codegen/expressions/variables.ts | 23 +++--- .../infrastructure/assignment-generator.ts | 70 +++++++++---------- src/codegen/llvm-generator.ts | 23 ++++-- src/codegen/statements/control-flow.ts | 20 +++--- .../types/collections/array/combine.ts | 6 +- .../types/collections/array/context.ts | 6 +- .../types/collections/array/mutators.ts | 30 ++++---- .../types/collections/array/reorder.ts | 18 ++--- src/codegen/types/collections/array/search.ts | 10 +-- src/codegen/types/collections/array/sort.ts | 6 +- src/codegen/types/collections/array/splice.ts | 6 +- src/parser-native/transformer.ts | 13 ++-- src/parser-ts/handlers/declarations.ts | 3 + 16 files changed, 163 insertions(+), 154 deletions(-) diff --git a/src/codegen/expressions/access/index.ts b/src/codegen/expressions/access/index.ts index 2d66f1dc..0c01d6b3 100644 --- a/src/codegen/expressions/access/index.ts +++ b/src/codegen/expressions/access/index.ts @@ -186,13 +186,13 @@ export class IndexAccessGenerator { ); const data = this.ctx.nextTemp(); - this.ctx.emit(`${data} = load i8**, i8*** ${dataPtr}, !tbaa !5`); + this.ctx.emit(`${data} = load i8**, i8*** ${dataPtr}`); const elemPtr = this.ctx.nextTemp(); this.ctx.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${data}, i32 ${index}`); const elem = this.ctx.nextTemp(); - this.ctx.emit(`${elem} = load i8*, i8** ${elemPtr}, !tbaa !5`); + this.ctx.emit(`${elem} = load i8*, i8** ${elemPtr}`); // Track that this loaded value is a string this.ctx.setVariableType(elem, "i8*"); return elem; @@ -217,14 +217,14 @@ export class IndexAccessGenerator { this.ctx.emit(`${dataPtr} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const data = this.ctx.nextTemp(); - this.ctx.emit(`${data} = load double*, double** ${dataPtr}, !tbaa !5`); + this.ctx.emit(`${data} = load double*, double** ${dataPtr}`); const elemPtr = this.ctx.nextTemp(); this.ctx.emit(`${elemPtr} = getelementptr inbounds double, double* ${data}, i32 ${index}`); // Load double element const elem = this.ctx.nextTemp(); - this.ctx.emit(`${elem} = load double, double* ${elemPtr}, !tbaa !4`); + this.ctx.emit(`${elem} = load double, double* ${elemPtr}`); this.ctx.setVariableType(elem, "double"); return elem; } @@ -328,7 +328,7 @@ export class IndexAccessGenerator { `${dataFieldPtr} = getelementptr inbounds %Uint8Array, %Uint8Array* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = this.ctx.nextTemp(); - this.ctx.emit(`${dataPtr} = load i8*, i8** ${dataFieldPtr}, !tbaa !5`); + this.ctx.emit(`${dataPtr} = load i8*, i8** ${dataFieldPtr}`); const elemPtr = this.ctx.nextTemp(); this.ctx.emit(`${elemPtr} = getelementptr inbounds i8, i8* ${dataPtr}, i32 ${index}`); @@ -354,7 +354,7 @@ export class IndexAccessGenerator { `${dataFieldPtr} = getelementptr inbounds %Uint8Array, %Uint8Array* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = this.ctx.nextTemp(); - this.ctx.emit(`${dataPtr} = load i8*, i8** ${dataFieldPtr}, !tbaa !5`); + this.ctx.emit(`${dataPtr} = load i8*, i8** ${dataFieldPtr}`); const indexDouble = this.ctx.generateExpression(expr.index, params); const indexType = this.ctx.getVariableType(indexDouble); @@ -679,7 +679,7 @@ export class IndexAccessGenerator { `${dataFieldPtr} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = this.ctx.nextTemp(); - this.ctx.emit(`${dataPtr} = load i8**, i8*** ${dataFieldPtr}, !tbaa !5`); + this.ctx.emit(`${dataPtr} = load i8**, i8*** ${dataFieldPtr}`); const indexDouble = this.ctx.generateExpression(expr.index, params); const indexType = this.ctx.getVariableType(indexDouble); @@ -697,7 +697,7 @@ export class IndexAccessGenerator { const elementPtr = this.ctx.nextTemp(); this.ctx.emit(`${elementPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i64 ${indexI64}`); - this.ctx.emit(`store i8* ${value}, i8** ${elementPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${elementPtr}`); return value; } @@ -736,7 +736,7 @@ export class IndexAccessGenerator { `${elementPtr} = getelementptr inbounds i8*, i8** ${dataAsPtrs}, i64 ${indexI64}`, ); - this.ctx.emit(`store i8* ${value}, i8** ${elementPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${elementPtr}`); return value; } @@ -753,7 +753,7 @@ export class IndexAccessGenerator { `${dataFieldPtr} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = this.ctx.nextTemp(); - this.ctx.emit(`${dataPtr} = load double*, double** ${dataFieldPtr}, !tbaa !5`); + this.ctx.emit(`${dataPtr} = load double*, double** ${dataFieldPtr}`); const indexDouble = this.ctx.generateExpression(expr.index, params); const indexType = this.ctx.getVariableType(indexDouble); @@ -774,7 +774,7 @@ export class IndexAccessGenerator { ); const dblValue = this.ctx.ensureDouble(value); - this.ctx.emit(`store double ${dblValue}, double* ${elementPtr}, !tbaa !4`); + this.ctx.emit(`store double ${dblValue}, double* ${elementPtr}`); return value; } diff --git a/src/codegen/expressions/access/member.ts b/src/codegen/expressions/access/member.ts index 3ea95a4b..4e1972bf 100644 --- a/src/codegen/expressions/access/member.ts +++ b/src/codegen/expressions/access/member.ts @@ -711,7 +711,7 @@ export class MemberAccessGenerator { const varPtr = this.ctx.getVariableAlloca((expr.object as VariableNode).name); const structPtr = this.ctx.nextTemp(); this.ctx.emit( - `${structPtr} = load %${structTypeName}*, %${structTypeName}** ${varPtr}, !tbaa !5`, + `${structPtr} = load %${structTypeName}*, %${structTypeName}** ${varPtr}`, ); const fieldPtr = this.ctx.nextTemp(); @@ -721,27 +721,27 @@ export class MemberAccessGenerator { if (propName === "nodePtr" || propName === "treePtr") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}`); this.ctx.setVariableType(value, "i8*"); return value; } else if (propType === "string") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}`); this.ctx.setVariableType(value, "i8*"); return value; } else if (propType === "number") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); this.ctx.setVariableType(value, "double"); return value; } else if (propType === "boolean") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); this.ctx.setVariableType(value, "double"); return value; } else if (propType === "string[]") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load %StringArray*, %StringArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %StringArray*, %StringArray** ${fieldPtr}`); this.ctx.setVariableType(value, "%StringArray*"); return value; } else { @@ -758,7 +758,7 @@ export class MemberAccessGenerator { const nestedInterface = nestedInterfaceResult as InterfaceInfo; const value = this.ctx.nextTemp(); if (isTypeAlias) { - this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}`); this.ctx.setVariableType(value, "i8*"); const keys: string[] = []; const types: string[] = []; @@ -773,7 +773,7 @@ export class MemberAccessGenerator { this.ctx.setJsonObjectMetadata(value, { keys, types, tsTypes, interfaceType: undefined }); } else { this.ctx.emit( - `${value} = load %${nestedTypeName}*, %${nestedTypeName}** ${fieldPtr}, !tbaa !5`, + `${value} = load %${nestedTypeName}*, %${nestedTypeName}** ${fieldPtr}`, ); this.ctx.setVariableType(value, `%${nestedTypeName}*`); } @@ -882,7 +882,7 @@ export class MemberAccessGenerator { `${fieldPtr} = getelementptr inbounds double, double* ${instancePtr}, i32 ${fieldInfo.index}`, ); const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); return value; } } @@ -906,7 +906,7 @@ export class MemberAccessGenerator { const fieldPtr = this.ctx.nextTemp(); this.ctx.emit(`${fieldPtr} = getelementptr inbounds double, double* ${instancePtr}, i32 0`); const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); return value; } else { throw new Error( @@ -931,7 +931,7 @@ export class MemberAccessGenerator { } if (fieldType === "string") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}`); this.ctx.setVariableType(value, "i8*"); if (tsType) { this.storeInterfaceMetadata(value, tsType); @@ -951,7 +951,7 @@ export class MemberAccessGenerator { return value; } else if (fieldType === "string[]") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load %StringArray*, %StringArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %StringArray*, %StringArray** ${fieldPtr}`); this.ctx.setVariableType(value, "%StringArray*"); return value; } else if (fieldType.endsWith("[]")) { @@ -963,36 +963,36 @@ export class MemberAccessGenerator { resolvedTsType !== "boolean[]"; const value = this.ctx.nextTemp(); if (isObjectArray) { - this.ctx.emit(`${value} = load %ObjectArray*, %ObjectArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %ObjectArray*, %ObjectArray** ${fieldPtr}`); this.ctx.setVariableType(value, "%ObjectArray*"); } else { - this.ctx.emit(`${value} = load %Array*, %Array** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %Array*, %Array** ${fieldPtr}`); this.ctx.setVariableType(value, "%Array*"); } return value; } else if (fieldType === "boolean") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); this.ctx.setVariableType(value, "double"); return value; } else if (tsType && tsType.startsWith("Map") { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load %StringSet*, %StringSet** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %StringSet*, %StringSet** ${fieldPtr}`); this.ctx.setVariableType(value, "%StringSet*"); return value; } else if (tsType && tsType.startsWith("Set<")) { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load %Set*, %Set** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %Set*, %Set** ${fieldPtr}`); this.ctx.setVariableType(value, "%Set*"); return value; } else if ( @@ -1000,7 +1000,7 @@ export class MemberAccessGenerator { (!tsType || tsType === "number" || tsType === "boolean") ) { const value = this.ctx.nextTemp(); - this.ctx.emit(`${value} = load double, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`${value} = load double, double* ${fieldPtr}`); this.ctx.setVariableType(value, "double"); return value; } else if (tsType && tsType.endsWith("[]")) { @@ -1008,10 +1008,10 @@ export class MemberAccessGenerator { tsType !== "number[]" && tsType !== "string[]" && tsType !== "boolean[]"; const value = this.ctx.nextTemp(); if (isObjectArray) { - this.ctx.emit(`${value} = load %ObjectArray*, %ObjectArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %ObjectArray*, %ObjectArray** ${fieldPtr}`); this.ctx.setVariableType(value, "%ObjectArray*"); } else { - this.ctx.emit(`${value} = load %Array*, %Array** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load %Array*, %Array** ${fieldPtr}`); this.ctx.setVariableType(value, "%Array*"); } return value; @@ -1025,11 +1025,11 @@ export class MemberAccessGenerator { const classNode = this.ctx.classGenGetClassFields(cleanTsType); if (classNode.length > 0) { const structType = `%${cleanTsType}_struct*`; - this.ctx.emit(`${value} = load ${structType}, ${structType}* ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load ${structType}, ${structType}* ${fieldPtr}`); this.ctx.setVariableType(value, structType); this.ctx.setActualClassType(value, cleanTsType); } else { - this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`${value} = load i8*, i8** ${fieldPtr}`); this.ctx.setVariableType(value, "i8*"); if (tsType) { let isKnownClass = false; @@ -1695,7 +1695,7 @@ export class MemberAccessGenerator { ); const data = this.ctx.nextTemp(); - this.ctx.emit(`${data} = load i8*, i8** ${dataPtr}, !tbaa !5`); + this.ctx.emit(`${data} = load i8*, i8** ${dataPtr}`); const dataAsPtrs = this.ctx.emitBitcast(data, "i8*", "i8**"); @@ -1703,7 +1703,7 @@ export class MemberAccessGenerator { this.ctx.emit(`${elemPtrPtr} = getelementptr inbounds i8*, i8** ${dataAsPtrs}, i32 ${index}`); const elemPtr = this.ctx.nextTemp(); - this.ctx.emit(`${elemPtr} = load i8*, i8** ${elemPtrPtr}, !tbaa !5`); + this.ctx.emit(`${elemPtr} = load i8*, i8** ${elemPtrPtr}`); const elemTyped = this.ctx.emitBitcast(elemPtr, "i8*", `${structType}*`); @@ -1715,8 +1715,7 @@ export class MemberAccessGenerator { ); const value = this.ctx.nextTemp(); - const tbaaField = propType === "double" ? "!tbaa !4" : "!tbaa !5"; - this.ctx.emit(`${value} = load ${propType}, ${propType}* ${fieldPtr}, ${tbaaField}`); + this.ctx.emit(`${value} = load ${propType}, ${propType}* ${fieldPtr}`); this.ctx.setVariableType(value, propType); if ( diff --git a/src/codegen/expressions/access/property-handlers.ts b/src/codegen/expressions/access/property-handlers.ts index 224006aa..dcfb3583 100644 --- a/src/codegen/expressions/access/property-handlers.ts +++ b/src/codegen/expressions/access/property-handlers.ts @@ -22,7 +22,7 @@ export function getArrayLength( `${lenPtr} = getelementptr inbounds ${arrayType}, ${arrayType}* ${arrayPtr}, i32 0, i32 1`, ); const lenI32 = ctx.nextTemp(); - ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}, !tbaa !7`); + ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}`); const len = ctx.nextTemp(); ctx.emit(`${len} = sitofp i32 ${lenI32} to double`); ctx.setVariableType(len, "double"); @@ -38,7 +38,7 @@ export function getStringArrayLength( `${lenPtr} = getelementptr inbounds %StringArray, %StringArray* ${stringArrayPtr}, i32 0, i32 1`, ); const lenI32 = ctx.nextTemp(); - ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}, !tbaa !7`); + ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}`); const len = ctx.nextTemp(); ctx.emit(`${len} = sitofp i32 ${lenI32} to double`); ctx.setVariableType(len, "double"); @@ -68,7 +68,7 @@ export function getArrayLengthFromPtr( `${lenPtr} = getelementptr inbounds ${arrayType}, ${arrayType}* ${arrayPtr}, i32 0, i32 1`, ); const lenI32 = ctx.nextTemp(); - ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}, !tbaa !7`); + ctx.emit(`${lenI32} = load i32, i32* ${lenPtr}`); const len = ctx.nextTemp(); ctx.emit(`${len} = sitofp i32 ${lenI32} to double`); ctx.setVariableType(len, "double"); diff --git a/src/codegen/expressions/variables.ts b/src/codegen/expressions/variables.ts index 178032aa..93c508c3 100644 --- a/src/codegen/expressions/variables.ts +++ b/src/codegen/expressions/variables.ts @@ -111,7 +111,7 @@ export class VariableExpressionGenerator { return this.loadArray(allocaReg, "%Array*", isPointerAlloca); } else if (llvmType === "i8*") { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}`); this.ctx.setVariableType(temp, "i8*"); return temp; } @@ -127,7 +127,7 @@ export class VariableExpressionGenerator { return this.loadArray(allocaReg, "%StringArray*", isPointerAlloca); } else if (llvmType === "i8*") { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}`); this.ctx.setVariableType(temp, "i8*"); return temp; } @@ -137,7 +137,7 @@ export class VariableExpressionGenerator { if (this.ctx.symbolTable.isUint8Array(name)) { const allocaReg = this.ctx.symbolTable.getAlloca(name)!; const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load %Uint8Array*, %Uint8Array** ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load %Uint8Array*, %Uint8Array** ${allocaReg}`); this.ctx.setVariableType(temp, "%Uint8Array*"); return temp; } @@ -166,7 +166,7 @@ export class VariableExpressionGenerator { // Handle __chadscript global if (name === "__chadscript") { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load double, double* @__chadscript, !tbaa !4`); + this.ctx.emit(`${temp} = load double, double* @__chadscript`); this.ctx.setVariableType(temp, "double"); return temp; } @@ -195,35 +195,35 @@ export class VariableExpressionGenerator { const ptrType = fields.length > 0 ? `%${classMeta.className}_struct*` : "double*"; const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load ${ptrType}, ${ptrType}* ${classMeta.ptr}, !tbaa !5`); + this.ctx.emit(`${temp} = load ${ptrType}, ${ptrType}* ${classMeta.ptr}`); this.ctx.setVariableType(temp, ptrType); return temp; } private loadRegex(allocaReg: string): string { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}`); this.ctx.setVariableType(temp, "i8*"); return temp; } private loadString(allocaReg: string): string { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load i8*, i8** ${allocaReg}`); this.ctx.setVariableType(temp, "i8*"); return temp; } private loadArray(allocaReg: string, arrayType: string, isPointerAlloca: boolean): string { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load ${arrayType}, ${arrayType}* ${allocaReg}, !tbaa !5`); + this.ctx.emit(`${temp} = load ${arrayType}, ${arrayType}* ${allocaReg}`); this.ctx.setVariableType(temp, arrayType); return temp; } private loadObject(objectMeta: ObjectMeta, _interfaceType?: string): string { const temp = this.ctx.nextTemp(); - this.ctx.emit(`${temp} = load i8*, i8** ${objectMeta.ptr}, !tbaa !5`); + this.ctx.emit(`${temp} = load i8*, i8** ${objectMeta.ptr}`); this.ctx.setVariableType(temp, "i8*"); return temp; } @@ -237,7 +237,7 @@ export class VariableExpressionGenerator { varType === "%TSParser*" || varType === "%TSLanguage*"; if (isTreeSitterType) { - this.ctx.emit(`${temp} = load double, double* ${allocaReg}, !tbaa !4`); + this.ctx.emit(`${temp} = load double, double* ${allocaReg}`); this.ctx.setVariableType(temp, varType); return temp; } @@ -245,8 +245,7 @@ export class VariableExpressionGenerator { varType = "i8*"; } const ptrToType = `${varType}*`; - const tbaaTag = varType === "double" ? "!tbaa !4" : "!tbaa !5"; - this.ctx.emit(`${temp} = load ${varType}, ${ptrToType} ${allocaReg}, ${tbaaTag}`); + this.ctx.emit(`${temp} = load ${varType}, ${ptrToType} ${allocaReg}`); this.ctx.setVariableType(temp, varType); return temp; } diff --git a/src/codegen/infrastructure/assignment-generator.ts b/src/codegen/infrastructure/assignment-generator.ts index feb2f53b..78f00a52 100644 --- a/src/codegen/infrastructure/assignment-generator.ts +++ b/src/codegen/infrastructure/assignment-generator.ts @@ -198,7 +198,7 @@ export class AssignmentGenerator { const objPtrPtr = this.ctx.getVariableAlloca(object.name)!; const objPtr = this.ctx.nextTemp(); - this.ctx.emit(`${objPtr} = load i8*, i8** ${objPtrPtr}, !tbaa !5`); + this.ctx.emit(`${objPtr} = load i8*, i8** ${objPtrPtr}`); const typedPtr = this.ctx.emitBitcast(objPtr, "i8*", `${structType}*`); @@ -208,11 +208,11 @@ export class AssignmentGenerator { ); if (propType === "i1") { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else if (propType === "double") { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else { - this.ctx.emit(`store ${propType} ${value}, ${propType}* ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store ${propType} ${value}, ${propType}* ${fieldPtr}`); } } @@ -284,13 +284,13 @@ export class AssignmentGenerator { `${fieldPtr} = getelementptr inbounds double, double* ${instancePtr}, i32 ${fieldIndex}`, ); this.ctx.emit( - `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`, + `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`, ); } } else if (fields.length === 0) { const fieldPtr = this.ctx.nextTemp(); this.ctx.emit(`${fieldPtr} = getelementptr inbounds double, double* ${instancePtr}, i32 0`); - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else { this.ctx.emitError( "Field '" + @@ -313,13 +313,13 @@ export class AssignmentGenerator { const enumResult = this.isEnumType(fiTsType); if (enumResult) { this.ctx.emit( - `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`, + `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`, ); return; } } if (fiType === null || fiType === undefined) { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); return; } @@ -343,26 +343,26 @@ export class AssignmentGenerator { } if (isAlreadyPointer) { - this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}`); } else { const strPtr = this.ctx.nextTemp(); this.ctx.emit(`${strPtr} = inttoptr i32 ${value} to i8*`); - this.ctx.emit(`store i8* ${strPtr}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${strPtr}, i8** ${fieldPtr}`); } } else if (fiType === "string[]") { - this.ctx.emit(`store %StringArray* ${value}, %StringArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %StringArray* ${value}, %StringArray** ${fieldPtr}`); } else if (fiType.endsWith("[]")) { - this.ctx.emit(`store %Array* ${value}, %Array** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %Array* ${value}, %Array** ${fieldPtr}`); } else if (fiType === "boolean") { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else if (hasTsType && fiTsType && fiTsType.startsWith("Map") { - this.ctx.emit(`store %StringSet* ${value}, %StringSet** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %StringSet* ${value}, %StringSet** ${fieldPtr}`); } else if (hasTsType && fiTsType && fiTsType.startsWith("Set<")) { - this.ctx.emit(`store %Set* ${value}, %Set** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %Set* ${value}, %Set** ${fieldPtr}`); } else if ( hasTsType && fiTsType && @@ -370,9 +370,9 @@ export class AssignmentGenerator { fiTsType !== "boolean" && !this.isEnumType(fiTsType) ) { - this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}`); } else { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } } @@ -386,7 +386,7 @@ export class AssignmentGenerator { const fiType = fi.type; if (fiType === null || fiType === undefined) { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); return; } @@ -411,26 +411,26 @@ export class AssignmentGenerator { } if (isAlreadyPointer) { - this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}`); } else { const strPtr = this.ctx.nextTemp(); this.ctx.emit(`${strPtr} = inttoptr i32 ${value} to i8*`); - this.ctx.emit(`store i8* ${strPtr}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${strPtr}, i8** ${fieldPtr}`); } } else if (fiType === "string[]") { - this.ctx.emit(`store %StringArray* ${value}, %StringArray** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %StringArray* ${value}, %StringArray** ${fieldPtr}`); } else if (fiType.endsWith("[]")) { - this.ctx.emit(`store %Array* ${value}, %Array** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %Array* ${value}, %Array** ${fieldPtr}`); } else if (fiType === "boolean") { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else if (hasTsType && fiTsType && fiTsType.startsWith("Map") { - this.ctx.emit(`store %StringSet* ${value}, %StringSet** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %StringSet* ${value}, %StringSet** ${fieldPtr}`); } else if (hasTsType && fiTsType && fiTsType.startsWith("Set<")) { - this.ctx.emit(`store %Set* ${value}, %Set** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store %Set* ${value}, %Set** ${fieldPtr}`); } else if ( hasTsType && fiTsType && @@ -438,9 +438,9 @@ export class AssignmentGenerator { fiTsType !== "boolean" && !this.isEnumType(fiTsType) ) { - this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store i8* ${value}, i8** ${fieldPtr}`); } else { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } } @@ -478,7 +478,7 @@ export class AssignmentGenerator { ); const data = this.ctx.nextTemp(); - this.ctx.emit(`${data} = load i8*, i8** ${dataPtr}, !tbaa !5`); + this.ctx.emit(`${data} = load i8*, i8** ${dataPtr}`); const dataAsPtrs = this.ctx.emitBitcast(data, "i8*", "i8**"); @@ -486,7 +486,7 @@ export class AssignmentGenerator { this.ctx.emit(`${elemPtrPtr} = getelementptr inbounds i8*, i8** ${dataAsPtrs}, i32 ${index}`); const elemPtr = this.ctx.nextTemp(); - this.ctx.emit(`${elemPtr} = load i8*, i8** ${elemPtrPtr}, !tbaa !5`); + this.ctx.emit(`${elemPtr} = load i8*, i8** ${elemPtrPtr}`); const elemTyped = this.ctx.emitBitcast(elemPtr, "i8*", `${structType}*`); @@ -498,9 +498,9 @@ export class AssignmentGenerator { const value = this.ctx.generateExpression(memberAccessValue.value, params); if (propType === "double") { - this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}, !tbaa !4`); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } else { - this.ctx.emit(`store ${propType} ${value}, ${propType}* ${fieldPtr}, !tbaa !5`); + this.ctx.emit(`store ${propType} ${value}, ${propType}* ${fieldPtr}`); } } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index 549b4de0..1a35959e 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -1417,8 +1417,10 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { private usesTreeSitter: boolean = false; // Tracks function names from user `declare function` to avoid duplicate - // LLVM declarations when a name overlaps with hardcoded runtime declarations - public declaredExternFunctions: Set; + // LLVM declarations when a name overlaps with hardcoded runtime declarations. + // Uses string[] instead of Set because Set.has() is unreliable in the + // native-compiled compiler (self-hosting). + public declaredExternFunctions: string[]; public sourceCode: string = ""; public filename: string = ""; @@ -1427,7 +1429,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { // Initialize complex fields in constructor (field initializers don't work in native code) this.externalFunctions = new Set(); - this.declaredExternFunctions = new Set(); + this.declaredExternFunctions = []; this.topLevelObjectVariables = new Map(); this.globalVariables = new Map(); this.importAliasNames = []; @@ -2677,7 +2679,14 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { finalParts.push(irParts[ipi]); } - finalParts.push("\n; TBAA metadata for alias analysis\n"); + // TBAA metadata — disabled for now. + // ChadScript's type-based alias analysis was causing -O2 segfaults because + // the optimizer incorrectly reorders loads/stores across struct fields when + // the TBAA hierarchy claims double and pointer types don't alias. Structs + // like FunctionNode contain both double and pointer fields, and the coarse + // type-based TBAA (without proper struct-path TBAA) leads to miscompilation. + // TODO: re-enable with struct-path TBAA once field-level aliasing is correct. + finalParts.push("\n; TBAA metadata (currently unused — see comment above)\n"); finalParts.push('!0 = !{!"ChadScript TBAA Root"}\n'); finalParts.push('!1 = !{!"omnipotent char", !0, i64 0}\n'); finalParts.push('!2 = !{!"double", !1, i64 0}\n'); @@ -2705,7 +2714,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { * declarations overlap with user declarations. */ private filterDuplicateDeclarations(ir: string): string { - if (this.declaredExternFunctions.size === 0) return ir; + if (this.declaredExternFunctions.length === 0) return ir; const lines = ir.split("\n"); const filtered: string[] = []; for (let dli = 0; dli < lines.length; dli++) { @@ -2717,7 +2726,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { const parenIdx = rest.indexOf("("); if (parenIdx !== -1) { const fnName = rest.substring(0, parenIdx); - if (this.declaredExternFunctions.has(fnName)) { + if (this.declaredExternFunctions.indexOf(fnName) !== -1) { continue; } } @@ -2801,7 +2810,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { } } - this.declaredExternFunctions.add(func.name); + this.declaredExternFunctions.push(func.name); return `declare ${retType} @${func.name}(${paramLlvmTypes.join(", ")})\n`; } diff --git a/src/codegen/statements/control-flow.ts b/src/codegen/statements/control-flow.ts index d5762825..b5b13181 100644 --- a/src/codegen/statements/control-flow.ts +++ b/src/codegen/statements/control-flow.ts @@ -460,7 +460,7 @@ export class ControlFlowGenerator { `${lenPtr} = getelementptr inbounds ${arrayType}, ${arrayType}* ${iterableValue}, i32 0, i32 1`, ); const lengthI32 = this.nextTemp(); - this.emit(`${lengthI32} = load i32, i32* ${lenPtr}, !tbaa !7`); + this.emit(`${lengthI32} = load i32, i32* ${lenPtr}`); const indexAlloca = this.ctx.nextAllocaReg("__forof_idx"); this.emit(`${indexAlloca} = alloca i32`); @@ -490,7 +490,7 @@ export class ControlFlowGenerator { this.ctx.setCurrentLabel(bodyLabel); // Load current element from array - // Get pointer to the data array (inbounds GEP + tbaa loads stay raw) + // Get pointer to the data array const dataPtr = this.nextTemp(); this.emit( `${dataPtr} = getelementptr inbounds ${arrayType}, ${arrayType}* ${iterableValue}, i32 0, i32 0`, @@ -498,14 +498,14 @@ export class ControlFlowGenerator { let dataArray: string; if (isStringSet || isStringArray) { dataArray = this.nextTemp(); - this.emit(`${dataArray} = load i8**, i8*** ${dataPtr}, !tbaa !5`); + this.emit(`${dataArray} = load i8**, i8*** ${dataPtr}`); } else if (isObjectArray) { const dataI8 = this.nextTemp(); - this.emit(`${dataI8} = load i8*, i8** ${dataPtr}, !tbaa !5`); + this.emit(`${dataI8} = load i8*, i8** ${dataPtr}`); dataArray = this.ctx.emitBitcast(dataI8, "i8*", "i8**"); } else { dataArray = this.nextTemp(); - this.emit(`${dataArray} = load double*, double** ${dataPtr}, !tbaa !5`); + this.emit(`${dataArray} = load double*, double** ${dataPtr}`); } // Load the element at current index @@ -1299,7 +1299,7 @@ export class ControlFlowGenerator { const lenPtr = this.nextTemp(); this.emit(`${lenPtr} = getelementptr inbounds %Array, %Array* ${iterableValue}, i32 0, i32 1`); const lengthI32 = this.nextTemp(); - this.emit(`${lengthI32} = load i32, i32* ${lenPtr}, !tbaa !7`); + this.emit(`${lengthI32} = load i32, i32* ${lenPtr}`); const indexAlloca = this.ctx.nextAllocaReg("__forof_idx"); this.emit(`${indexAlloca} = alloca i32`); @@ -1342,11 +1342,11 @@ export class ControlFlowGenerator { this.ctx.emitLabel(bodyLabel); this.ctx.setCurrentLabel(bodyLabel); - // inbounds GEP + tbaa loads stay raw + // inbounds GEP loads stay raw const dataPtr = this.nextTemp(); this.emit(`${dataPtr} = getelementptr inbounds %Array, %Array* ${iterableValue}, i32 0, i32 0`); const dataArray = this.nextTemp(); - this.emit(`${dataArray} = load double*, double** ${dataPtr}, !tbaa !5`); + this.emit(`${dataArray} = load double*, double** ${dataPtr}`); const elemPtrRaw = this.ctx.emitBitcast(dataArray, "double*", "i8**"); @@ -2088,13 +2088,13 @@ export class ControlFlowGenerator { this.ctx.emitLabel(bodyLabel); this.ctx.setCurrentLabel(bodyLabel); - // inbounds GEP + tbaa loads stay raw + // inbounds GEP loads stay raw const dataFieldPtr = this.nextTemp(); this.emit( `${dataFieldPtr} = getelementptr inbounds %Array, %Array* ${iterableValue}, i32 0, i32 2`, ); const dataPtr = this.nextTemp(); - this.emit(`${dataPtr} = load double*, double** ${dataFieldPtr}, !tbaa !5`); + this.emit(`${dataPtr} = load double*, double** ${dataFieldPtr}`); const dataCast = this.ctx.emitBitcast(dataPtr, "double*", "i8**"); const indexI64 = this.nextTemp(); diff --git a/src/codegen/types/collections/array/combine.ts b/src/codegen/types/collections/array/combine.ts index e36d63a6..15616296 100644 --- a/src/codegen/types/collections/array/combine.ts +++ b/src/codegen/types/collections/array/combine.ts @@ -273,16 +273,14 @@ function generateStringArrayLiteralWithSpread( gen.emit( `${srcLenPtr} = getelementptr inbounds %StringArray, %StringArray* ${src.ptr}, i32 0, i32 1`, ); - // load with !tbaa metadata must stay as raw emit const srcLen = gen.nextTemp(); - gen.emit(`${srcLen} = load i32, i32* ${srcLenPtr}, !tbaa !7`); + gen.emit(`${srcLen} = load i32, i32* ${srcLenPtr}`); const srcDataField = gen.nextTemp(); gen.emit( `${srcDataField} = getelementptr inbounds %StringArray, %StringArray* ${src.ptr}, i32 0, i32 0`, ); - // load with !tbaa metadata must stay as raw emit const srcDataPtr = gen.nextTemp(); - gen.emit(`${srcDataPtr} = load i8**, i8*** ${srcDataField}, !tbaa !5`); + gen.emit(`${srcDataPtr} = load i8**, i8*** ${srcDataField}`); const checkLabel = gen.nextLabel("spread_check"); const bodyLabel = gen.nextLabel("spread_body"); diff --git a/src/codegen/types/collections/array/context.ts b/src/codegen/types/collections/array/context.ts index 1effc1a7..56db3930 100644 --- a/src/codegen/types/collections/array/context.ts +++ b/src/codegen/types/collections/array/context.ts @@ -10,7 +10,7 @@ interface ExprBase { type: string; } -/** Loads %Array (numeric) length and data pointer. Includes tbaa metadata for optimizer hints. */ +/** Loads %Array (numeric) length and data pointer. */ export function loadArrayMeta( gen: IGeneratorContext, arrayPtr: string, @@ -18,11 +18,11 @@ export function loadArrayMeta( const lenPtr = gen.nextTemp(); gen.emit(`${lenPtr} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 1`); const length = gen.nextTemp(); - gen.emit(`${length} = load i32, i32* ${lenPtr}, !tbaa !7`); + gen.emit(`${length} = load i32, i32* ${lenPtr}`); const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); return { length, dataPtr }; } diff --git a/src/codegen/types/collections/array/mutators.ts b/src/codegen/types/collections/array/mutators.ts index 6b4eb04f..23164316 100644 --- a/src/codegen/types/collections/array/mutators.ts +++ b/src/codegen/types/collections/array/mutators.ts @@ -1,5 +1,5 @@ // Array mutator operations: push, pop. -// Uses structured IR builders where possible; raw emit() for inbounds GEP, tbaa, intrinsics, etc. +// Uses structured IR builders where possible; raw emit() for inbounds GEP, intrinsics, etc. import { MethodCallNode, VariableNode } from "../../../../ast/types.js"; import { IGeneratorContext } from "./context.js"; @@ -133,13 +133,13 @@ function generateIntArrayPop(gen: IGeneratorContext, arrayPtr: string): string { const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); // Load last element const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds double, double* ${dataPtr}, i32 ${lastIndex}`); const lastElem = gen.nextTemp(); - gen.emit(`${lastElem} = load double, double* ${elemPtr}, !tbaa !4`); + gen.emit(`${lastElem} = load double, double* ${elemPtr}`); // Decrement length gen.emitStore("i32", lastIndex, lenPtr); @@ -193,13 +193,13 @@ function generateStringArrayPop(gen: IGeneratorContext, arrayPtr: string): strin `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); // Load last element const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${lastIndex}`); const lastElem = gen.nextTemp(); - gen.emit(`${lastElem} = load i8*, i8** ${elemPtr}, !tbaa !5`); + gen.emit(`${lastElem} = load i8*, i8** ${elemPtr}`); // Decrement length gen.emitStore("i32", lastIndex, lenPtr); @@ -246,13 +246,13 @@ function generatePointerArrayPop(gen: IGeneratorContext, arrayPtr: string): stri const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${castPtr}, i32 0, i32 0`); const dataPtrRaw = gen.nextTemp(); - gen.emit(`${dataPtrRaw} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtrRaw} = load double*, double** ${dataPtrField}`); const dataPtr = gen.emitBitcast(dataPtrRaw, "double*", "i8**"); const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${lastIndex}`); const lastElem = gen.nextTemp(); - gen.emit(`${lastElem} = load i8*, i8** ${elemPtr}, !tbaa !5`); + gen.emit(`${lastElem} = load i8*, i8** ${elemPtr}`); gen.emitStore("i32", lastIndex, lenPtr); @@ -341,13 +341,13 @@ function generateIntArrayPush(gen: IGeneratorContext, arrayPtr: string, value: s const dataPtrField2 = gen.nextTemp(); gen.emit(`${dataPtrField2} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField2}`); // Store value at current length index const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds double, double* ${dataPtr}, i32 ${currentLen}`); const dblValue = gen.ensureDouble(value); - gen.emit(`store double ${dblValue}, double* ${elemPtr}, !tbaa !4`); + gen.emit(`store double ${dblValue}, double* ${elemPtr}`); // Increment length const newLen = gen.nextTemp(); @@ -440,12 +440,12 @@ function generateStringArrayPush(gen: IGeneratorContext, arrayPtr: string, value `${dataPtrField2} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField2}`); // Store value at current length index const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${currentLen}`); - gen.emit(`store i8* ${value}, i8** ${elemPtr}, !tbaa !5`); + gen.emit(`store i8* ${value}, i8** ${elemPtr}`); // Increment length const newLen = gen.nextTemp(); @@ -523,13 +523,13 @@ function generatePointerArrayPush( const dataPtrField2 = gen.nextTemp(); gen.emit(`${dataPtrField2} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtrRaw = gen.nextTemp(); - gen.emit(`${dataPtrRaw} = load double*, double** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtrRaw} = load double*, double** ${dataPtrField2}`); const dataPtr = gen.emitBitcast(dataPtrRaw, "double*", "i8**"); const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${currentLen}`); const valueAsI8 = gen.emitBitcast(value, valueType, "i8*"); - gen.emit(`store i8* ${valueAsI8}, i8** ${elemPtr}, !tbaa !5`); + gen.emit(`store i8* ${valueAsI8}, i8** ${elemPtr}`); const newLen = gen.nextTemp(); gen.emit(`${newLen} = add i32 ${currentLen}, 1`); @@ -613,13 +613,13 @@ function generateObjectArrayPush( `${dataPtrField2} = getelementptr inbounds %ObjectArray, %ObjectArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtrRaw = gen.nextTemp(); - gen.emit(`${dataPtrRaw} = load i8*, i8** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtrRaw} = load i8*, i8** ${dataPtrField2}`); const dataPtr = gen.emitBitcast(dataPtrRaw, "i8*", "i8**"); const elemPtr = gen.nextTemp(); gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${currentLen}`); const valueAsI8 = gen.emitBitcast(value, valueType, "i8*"); - gen.emit(`store i8* ${valueAsI8}, i8** ${elemPtr}, !tbaa !5`); + gen.emit(`store i8* ${valueAsI8}, i8** ${elemPtr}`); const newLen = gen.nextTemp(); gen.emit(`${newLen} = add i32 ${currentLen}, 1`); diff --git a/src/codegen/types/collections/array/reorder.ts b/src/codegen/types/collections/array/reorder.ts index 688c8cf9..17ea3eb7 100644 --- a/src/codegen/types/collections/array/reorder.ts +++ b/src/codegen/types/collections/array/reorder.ts @@ -1,5 +1,5 @@ // Array reorder operations: reverse, shift, unshift. -// Uses structured IR builders where possible; raw emit() for inbounds GEP, tbaa, intrinsics, etc. +// Uses structured IR builders where possible; raw emit() for inbounds GEP, intrinsics, etc. import { MethodCallNode, VariableNode } from "../../../../ast/types.js"; import { IGeneratorContext } from "./context.js"; @@ -46,7 +46,7 @@ function generateNumericArrayReverseInPlace(gen: IGeneratorContext, arrayPtr: st const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const half = gen.nextTemp(); gen.emit(`${half} = sdiv i32 ${length}, 2`); @@ -107,7 +107,7 @@ function generateStringArrayReverseInPlace(gen: IGeneratorContext, arrayPtr: str `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const half = gen.nextTemp(); gen.emit(`${half} = sdiv i32 ${length}, 2`); @@ -205,7 +205,7 @@ function generateNumericArrayShift(gen: IGeneratorContext, arrayPtr: string): st const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const firstElem = gen.emitLoad("double", dataPtr); @@ -260,7 +260,7 @@ function generateStringArrayShift(gen: IGeneratorContext, arrayPtr: string): str `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const firstElem = gen.emitLoad("i8*", dataPtr); @@ -380,7 +380,7 @@ function generateNumericArrayUnshift( const dataPtrField2 = gen.nextTemp(); gen.emit(`${dataPtrField2} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField2}`); const destPtr = gen.nextTemp(); gen.emit(`${destPtr} = getelementptr inbounds double, double* ${dataPtr}, i32 1`); @@ -395,7 +395,7 @@ function generateNumericArrayUnshift( ); const dblValue = gen.ensureDouble(value); - gen.emit(`store double ${dblValue}, double* ${dataPtr}, !tbaa !4`); + gen.emit(`store double ${dblValue}, double* ${dataPtr}`); const newLen = gen.nextTemp(); gen.emit(`${newLen} = add i32 ${currentLen}, 1`); @@ -473,7 +473,7 @@ function generateStringArrayUnshift( `${dataPtrField2} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField2}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField2}`); const destPtr = gen.nextTemp(); gen.emit(`${destPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 1`); @@ -488,7 +488,7 @@ function generateStringArrayUnshift( `call void @llvm.memmove.p0i8.p0i8.i64(i8* ${destI8}, i8* ${srcI8}, i64 ${moveBytes}, i1 false)`, ); - gen.emit(`store i8* ${value}, i8** ${dataPtr}, !tbaa !5`); + gen.emit(`store i8* ${value}, i8** ${dataPtr}`); const newLen = gen.nextTemp(); gen.emit(`${newLen} = add i32 ${currentLen}, 1`); diff --git a/src/codegen/types/collections/array/search.ts b/src/codegen/types/collections/array/search.ts index abe49070..34ee9095 100644 --- a/src/codegen/types/collections/array/search.ts +++ b/src/codegen/types/collections/array/search.ts @@ -1,5 +1,5 @@ // Array search operations: indexOf, findIndex. -// Uses structured IR builders where possible; raw emit() for inbounds GEP, tbaa, intrinsics, etc. +// Uses structured IR builders where possible; raw emit() for inbounds GEP, intrinsics, etc. import { MethodCallNode, VariableNode } from "../../../../ast/types.js"; import { IGeneratorContext } from "./context.js"; @@ -61,7 +61,7 @@ function generateNumericArrayIndexOf( const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const resultPtr = gen.nextTemp(); gen.emit(`${resultPtr} = alloca i32`); @@ -128,7 +128,7 @@ function generateStringArrayIndexOf( `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const resultPtr = gen.nextTemp(); gen.emit(`${resultPtr} = alloca i32`); @@ -233,7 +233,7 @@ function generateNumericArrayFindIndex( const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const resultPtr = gen.nextTemp(); gen.emit(`${resultPtr} = alloca i32`); @@ -304,7 +304,7 @@ function generateStringArrayFindIndex( `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const resultPtr = gen.nextTemp(); gen.emit(`${resultPtr} = alloca i32`); diff --git a/src/codegen/types/collections/array/sort.ts b/src/codegen/types/collections/array/sort.ts index aac5040d..c5dadf60 100644 --- a/src/codegen/types/collections/array/sort.ts +++ b/src/codegen/types/collections/array/sort.ts @@ -107,7 +107,7 @@ function generateDefaultNumericSort(gen: ArraySortContext, arrayPtr: string): st const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const dataI8 = gen.nextTemp(); gen.emit(`${dataI8} = bitcast double* ${dataPtr} to i8*`); @@ -135,7 +135,7 @@ function generateDefaultStringSort(gen: ArraySortContext, arrayPtr: string): str `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const dataI8 = gen.nextTemp(); gen.emit(`${dataI8} = bitcast i8** ${dataPtr} to i8*`); @@ -164,7 +164,7 @@ function generateNumericSortWithFn( const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const checkLabel = gen.nextLabel("sort_check"); const outerBody = gen.nextLabel("sort_outer"); diff --git a/src/codegen/types/collections/array/splice.ts b/src/codegen/types/collections/array/splice.ts index de3bd52e..7a1d4ea6 100644 --- a/src/codegen/types/collections/array/splice.ts +++ b/src/codegen/types/collections/array/splice.ts @@ -66,9 +66,8 @@ function generateNumericArraySplice( const dataPtrField = gen.nextTemp(); gen.emit(`${dataPtrField} = getelementptr inbounds %Array, %Array* ${arrayPtr}, i32 0, i32 0`); - // !tbaa metadata on load -- keep as raw emit const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load double*, double** ${dataPtrField}`); const startNeg = gen.emitIcmp("slt", "i32", startRaw, "0"); const startFromEnd = gen.nextTemp(); @@ -172,9 +171,8 @@ function generateStringArraySplice( gen.emit( `${dataPtrField} = getelementptr inbounds %StringArray, %StringArray* ${arrayPtr}, i32 0, i32 0`, ); - // !tbaa metadata on load -- keep as raw emit const dataPtr = gen.nextTemp(); - gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}, !tbaa !5`); + gen.emit(`${dataPtr} = load i8**, i8*** ${dataPtrField}`); const startNeg = gen.emitIcmp("slt", "i32", startRaw, "0"); const startFromEnd = gen.nextTemp(); diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 63a02fc4..19e872dc 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -299,6 +299,7 @@ function handleAmbientDeclaration(node: TreeSitterNode, ast: AST): void { typeParameters: undefined, async: undefined, parameters: undefined, + loc: undefined, declare: true, }; ast.functions.push(func); @@ -2277,11 +2278,12 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null const paramTypes = paramsNode ? extractParamTypes(paramsNode) : undefined; const parameters = paramsNode ? extractFunctionParameters(paramsNode) : undefined; - // Include `declare: false` so this creation site matches the struct layout - // of handleAmbientDeclaration's creation site (which has declare: true). - // Native code determines struct layout from object literals — all creation - // sites for the same interface must have the same fields. - // IMPORTANT: must be `false` not `undefined` — native codegen may skip + // All creation sites for FunctionNode must include every field from the interface + // definition in the same order. The native compiler uses the interface field list + // to compute GEP indices (declare = index 9), but determines struct size from + // object literals. Omitting `loc` puts `declare` at index 8 in memory while the + // codegen accesses index 9 — an out-of-bounds read that segfaults. + // IMPORTANT: `declare` must be `false` not `undefined` — native codegen may skip // the field slot entirely for `undefined`, causing struct size mismatch. return { name, @@ -2292,6 +2294,7 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null typeParameters, async: isAsync || undefined, parameters, + loc: undefined, declare: false, }; } diff --git a/src/parser-ts/handlers/declarations.ts b/src/parser-ts/handlers/declarations.ts index ff6849bd..3b50fa0d 100644 --- a/src/parser-ts/handlers/declarations.ts +++ b/src/parser-ts/handlers/declarations.ts @@ -66,6 +66,8 @@ export function transformFunctionDeclaration( const isAsync = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) || false; + // Must include all FunctionNode interface fields to match struct layout of + // declare function creation site in transformer.ts (see FunctionNode in types.ts) return { name, params, @@ -76,6 +78,7 @@ export function transformFunctionDeclaration( async: isAsync || undefined, parameters: parameters.length > 0 ? parameters : undefined, loc: getLoc(node), + declare: false, }; } From 45ceaeab33d41003cd3972cfca1d6cfc3c25f494 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 18:17:56 -0800 Subject: [PATCH 09/10] prettier formatting --- src/codegen/expressions/access/member.ts | 8 ++------ src/codegen/infrastructure/assignment-generator.ts | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/codegen/expressions/access/member.ts b/src/codegen/expressions/access/member.ts index 4e1972bf..fad56713 100644 --- a/src/codegen/expressions/access/member.ts +++ b/src/codegen/expressions/access/member.ts @@ -710,9 +710,7 @@ export class MemberAccessGenerator { const propName = propField.name; const varPtr = this.ctx.getVariableAlloca((expr.object as VariableNode).name); const structPtr = this.ctx.nextTemp(); - this.ctx.emit( - `${structPtr} = load %${structTypeName}*, %${structTypeName}** ${varPtr}`, - ); + this.ctx.emit(`${structPtr} = load %${structTypeName}*, %${structTypeName}** ${varPtr}`); const fieldPtr = this.ctx.nextTemp(); this.ctx.emit( @@ -772,9 +770,7 @@ export class MemberAccessGenerator { } this.ctx.setJsonObjectMetadata(value, { keys, types, tsTypes, interfaceType: undefined }); } else { - this.ctx.emit( - `${value} = load %${nestedTypeName}*, %${nestedTypeName}** ${fieldPtr}`, - ); + this.ctx.emit(`${value} = load %${nestedTypeName}*, %${nestedTypeName}** ${fieldPtr}`); this.ctx.setVariableType(value, `%${nestedTypeName}*`); } return value; diff --git a/src/codegen/infrastructure/assignment-generator.ts b/src/codegen/infrastructure/assignment-generator.ts index 78f00a52..8c01c3a1 100644 --- a/src/codegen/infrastructure/assignment-generator.ts +++ b/src/codegen/infrastructure/assignment-generator.ts @@ -283,9 +283,7 @@ export class AssignmentGenerator { this.ctx.emit( `${fieldPtr} = getelementptr inbounds double, double* ${instancePtr}, i32 ${fieldIndex}`, ); - this.ctx.emit( - `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`, - ); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); } } else if (fields.length === 0) { const fieldPtr = this.ctx.nextTemp(); @@ -312,9 +310,7 @@ export class AssignmentGenerator { if (fiTsType) { const enumResult = this.isEnumType(fiTsType); if (enumResult) { - this.ctx.emit( - `store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`, - ); + this.ctx.emit(`store double ${this.ctx.ensureDouble(value)}, double* ${fieldPtr}`); return; } } From 6daad01ddf406da067f422bb059634124d6f3fc9 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Feb 2026 18:20:53 -0800 Subject: [PATCH 10/10] add docs for jsx, declare function ffi, and --link-obj --- docs/getting-started/cli.md | 1 + docs/language/limitations.md | 56 ++++++++++++++++++++++++++++++++++ docs/language/type-mappings.md | 14 +++++++++ 3 files changed, 71 insertions(+) diff --git a/docs/getting-started/cli.md b/docs/getting-started/cli.md index f77ff37a..c3077a16 100644 --- a/docs/getting-started/cli.md +++ b/docs/getting-started/cli.md @@ -87,6 +87,7 @@ SDKs are installed to `~/.chadscript/targets//`. | `--emit-llvm`, `-S` | Output LLVM IR only (no binary) | | `--keep-temps` | Keep intermediate files (`.ll`, `.o`) | | `-fsanitize=address` | Build with AddressSanitizer (ASAN) | +| `--link-obj ` | Link an external object file or static library (repeatable) | ## Cross-Compilation diff --git a/docs/language/limitations.md b/docs/language/limitations.md index e0e69437..8ec9fab7 100644 --- a/docs/language/limitations.md +++ b/docs/language/limitations.md @@ -39,6 +39,7 @@ ChadScript supports a practical subset of TypeScript. All types must be known at | Default parameters | Supported | | Rest parameters (`...args`) | Supported | | Closures | Supported (capture by value, not by reference) | +| `declare function` (FFI) | Supported (see [FFI](#foreign-function-interface-ffi)) | | Async generators / `for await...of` | Not supported | ## Types and Data Structures @@ -111,6 +112,61 @@ ChadScript supports a practical subset of TypeScript. All types must be known at | `.then()`, `.catch()`, `.finally()` | Supported | | `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` | Supported | +## JSX + +| Feature | Status | +|---------|--------| +| JSX elements (``) | Supported (desugared to `createElement()` calls) | +| Fragments (`<>...`) | Supported | +| Expression attributes (`prop={expr}`) | Supported | +| String attributes (`prop="text"`) | Supported | +| Self-closing elements (``) | Supported | +| Nested elements | Supported | + +JSX is desugared at parse time into `createElement(tag, props, children)` calls. You provide the `createElement` function — ChadScript doesn't ship a framework. Files must use `.tsx` extension. + +```tsx +interface Props { + text: string; + color: number; +} + +function createElement(tag: string, props: Props, children: string[]): string { + // your rendering logic here + return tag; +} + +const ui =