├── .gitignore ├── ChicoryErrorListener.ts ├── ChicoryTypeCheckerVisitor.ts ├── ChicoryTypes.ts ├── ChicoryVisitor.ts ├── Prelude.ts ├── README.md ├── TypeEnvironment.ts ├── bunfig.toml ├── chicPlugin.ts ├── compile.ts ├── env.d.ts ├── execute.ts ├── generate-chicory-dom-types.ts ├── grammar └── Chicory.g4 ├── package-lock.json ├── package.json ├── sample.chic └── tests ├── 1. assignments.test.ts ├── 10. comments.test.ts ├── 11. imports and exports.test.ts ├── 12. records tuples and arrays.test.ts ├── 13. function types.test.ts ├── 14. generic types.test.ts ├── 15. exhaustive match.test.ts ├── 16. type annotations.test.ts ├── 17. arrays.test.ts ├── 18. prelude.test.ts ├── 19. optional records.test.ts ├── 2. operations.test.ts ├── 20. nested types.test.ts ├── 21. jsx expressions.test.ts ├── 22. global binding.test.ts ├── 23. void unification.test.ts ├── 24. ignored params.test.ts ├── 3. member expressions.test.ts ├── 4. if expressions.test.ts ├── 5. function expressions.test.ts ├── 6. match expressions.test.ts ├── 7. call expressions.test.ts ├── 8. jsx expressions.test.ts └── 9. type definitions.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | generated/ 3 | build/ -------------------------------------------------------------------------------- /ChicoryErrorListener.ts: -------------------------------------------------------------------------------- 1 | import { ANTLRErrorListener, ATNSimulator, RecognitionException, Recognizer, Token } from "antlr4ng"; 2 | import { SyntaxError } from "./env"; 3 | 4 | // Custom error listener to provide better error messages 5 | export class ChicoryErrorListener { 6 | private errors: SyntaxError[] = []; 7 | 8 | syntaxError(recognizer: Recognizer, offendingSymbol: S | null, line: number, charPositionInLine: number, msg: string, e: RecognitionException | null) 9 | : void { 10 | // Create a context-like object for the error 11 | const range = { 12 | start: { line, character: charPositionInLine }, 13 | end: { line, character: charPositionInLine + (offendingSymbol?.text?.length || 1) - 1 } 14 | }; 15 | 16 | this.errors.push({ message: "Syntax Error: " + msg, range }); 17 | } 18 | 19 | getErrors(): SyntaxError[] { 20 | return this.errors; 21 | } 22 | 23 | clearErrors(): void { 24 | this.errors = []; 25 | } 26 | 27 | reportAmbiguity() {} 28 | reportAttemptingFullContext() {} 29 | reportContextSensitivity() {} 30 | } -------------------------------------------------------------------------------- /ChicoryTypes.ts: -------------------------------------------------------------------------------- 1 | import { ChicoryType } from "./env"; 2 | 3 | // Primitive Types 4 | export class StringTypeClass implements ChicoryType { 5 | static instance = new StringTypeClass(); 6 | private constructor() {} // Make constructor private to enforce singleton 7 | toString() { 8 | return "string"; 9 | } 10 | } 11 | 12 | export class NumberTypeClass implements ChicoryType { 13 | static instance = new NumberTypeClass(); 14 | private constructor() {} 15 | toString() { 16 | return "number"; 17 | } 18 | } 19 | 20 | export class BooleanTypeClass implements ChicoryType { 21 | static instance = new BooleanTypeClass(); 22 | private constructor() {} 23 | toString() { 24 | return "boolean"; 25 | } 26 | } 27 | 28 | export class UnitTypeClass implements ChicoryType { 29 | // Represents 'void' or '()' 30 | static instance = new UnitTypeClass(); 31 | private constructor() {} 32 | toString() { 33 | return "void"; 34 | } 35 | } 36 | 37 | // Function Type 38 | export class FunctionType implements ChicoryType { 39 | constructor( 40 | public paramTypes: ChicoryType[], 41 | public returnType: ChicoryType, 42 | public constructorName?: string 43 | ) {} 44 | toString() { 45 | if (this.constructorName) { 46 | return `${this.returnType} ~> ${this.constructorName}`; 47 | } 48 | const params = this.paramTypes.map((p) => p.toString()).join(", "); 49 | return `(${params}) => ${this.returnType.toString()}`; 50 | } 51 | } 52 | 53 | // Record Type Field Definition 54 | export interface RecordField { 55 | type: ChicoryType; 56 | optional: boolean; 57 | } 58 | 59 | // Record Type 60 | export class RecordType implements ChicoryType { 61 | constructor(public fields: Map) {} // Store RecordField objects 62 | toString() { 63 | const fieldStrings = Array.from(this.fields.entries()).map( 64 | ([key, field]) => `${key}${field.optional ? "?" : ""}: ${field.type.toString()}` // Add '?' if optional 65 | ); 66 | return `{ ${fieldStrings.join(", ")} }`; 67 | } 68 | } 69 | 70 | // Tuple Type 71 | export class TupleType implements ChicoryType { 72 | constructor(public elementTypes: ChicoryType[]) {} 73 | toString() { 74 | return `[${this.elementTypes.map((e) => e.toString()).join(", ")}]`; 75 | } 76 | } 77 | 78 | // Array Type 79 | export class ArrayType implements ChicoryType { 80 | readonly kind = "Array"; 81 | constructor(public elementType: ChicoryType) {} 82 | 83 | toString(): string { 84 | const elementStr = this.elementType.toString(); 85 | // Parenthesize complex inner types for clarity 86 | if ( 87 | this.elementType instanceof FunctionType || 88 | this.elementType instanceof TupleType || 89 | // this.elementType instanceof ArrayType || // Handle T[][] correctly 90 | (this.elementType instanceof GenericType && 91 | this.elementType.typeArguments.length > 0) || 92 | // Add other conditions if needed, e.g., ADT type strings that might conflict 93 | this.elementType instanceof AdtType // Maybe parenthesize ADTs too? e.g., (MyADT)[] vs potential ambiguity 94 | ) { 95 | return `(${elementStr})[]`; 96 | } 97 | return `${elementStr}[]`; 98 | } 99 | } 100 | 101 | // ADT Type 102 | export class AdtType implements ChicoryType { 103 | constructor( 104 | public name: string, 105 | public typeParameters: ChicoryType[] = [] 106 | ) {} 107 | toString() { 108 | if (this.typeParameters.length === 0) { 109 | return this.name; 110 | } 111 | return `${this.name}<${this.typeParameters.map(t => t.toString()).join(", ")}>`; 112 | } 113 | } 114 | 115 | // Generic Type (Simplified for now) 116 | export class GenericType implements ChicoryType { 117 | constructor(public id: number, public name: string, public typeArguments: ChicoryType[]) {} 118 | 119 | toString() { 120 | if (this.typeArguments.length === 0) { 121 | return this.name; 122 | } 123 | const args = this.typeArguments.map((t) => t.toString()).join(", "); 124 | return `${this.name}<${args}>`; 125 | } 126 | } 127 | 128 | // Type Variable (For future use with type inference) 129 | export class TypeVariable implements ChicoryType { 130 | constructor(public id: number, public name: string) {} 131 | toString() { 132 | return this.name; 133 | } 134 | } 135 | 136 | // String Literal Type 137 | export class StringLiteralType implements ChicoryType { 138 | constructor(public value: string) {} 139 | toString() { 140 | return `"${this.value}"`; // Represent as "value" 141 | } 142 | } 143 | 144 | // Literal Union Type 145 | export class LiteralUnionType implements ChicoryType { 146 | constructor(public values: Set) {} // Store the actual string values 147 | toString() { 148 | return Array.from(this.values).map(v => `"${v}"`).join(" | "); 149 | } 150 | } 151 | 152 | // JSX Element Type 153 | export class JsxElementType implements ChicoryType { 154 | // Represents the result of a JSX expression, holding the type of its expected props. 155 | constructor(public propsType: RecordType) {} 156 | toString() { 157 | const props = this.propsType.toString() 158 | const propsMessage = props.length > 23 159 | ? props.slice(0, 20) + "..." 160 | : props 161 | // Indicate the props type it expects 162 | return `JsxElement<${propsMessage}>`; 163 | } 164 | } 165 | 166 | // Unknown Type (For errors or incomplete type information) 167 | export class UnknownTypeClass implements ChicoryType { 168 | static instance = new UnknownTypeClass(); 169 | private constructor() {} 170 | toString() { 171 | return "unknown"; 172 | } 173 | } 174 | 175 | // Constructor Definition (for ADTs) 176 | export interface ConstructorDefinition { 177 | adtName: string; 178 | name: string; 179 | type: ChicoryType; // Can be FunctionType (if constructor takes args) or the ADT Type (if no args) 180 | } 181 | 182 | export function typesAreEqual(type1: ChicoryType, type2: ChicoryType): boolean { 183 | if (type1 === type2) { 184 | return true; 185 | } 186 | // TODO: 187 | // if (type1.kind !== type2.kind) { 188 | // // Allow unifying a GenericType placeholder like 'T' with a concrete type during inference 189 | // // This might need refinement based on how generics are fully handled. 190 | // if (type1 instanceof TypeVariable || type2 instanceof TypeVariable) return true; 191 | // if (type1 instanceof GenericType && type1.typeArguments.length === 0) return true; 192 | // if (type2 instanceof GenericType && type2.typeArguments.length === 0) return true; 193 | 194 | // return false; 195 | // } 196 | 197 | if (type1 instanceof RecordType && type2 instanceof RecordType) { 198 | if (type1.fields.size !== type2.fields.size) { 199 | return false; 200 | } 201 | 202 | // Compare fields based on type and optional flag 203 | for (const [key, field1] of type1.fields) { 204 | const field2 = type2.fields.get(key); 205 | if (!field2 || field1.optional !== field2.optional || !typesAreEqual(field1.type, field2.type)) { 206 | return false; 207 | } 208 | } 209 | return true; 210 | } else if (type1 instanceof TupleType && type2 instanceof TupleType) { 211 | if (type1.elementTypes.length !== type2.elementTypes.length) { 212 | return false; 213 | } 214 | for (let i = 0; i < type1.elementTypes.length; i++) { 215 | if (!typesAreEqual(type1.elementTypes[i], type2.elementTypes[i])) { 216 | return false; 217 | } 218 | } 219 | return true; 220 | } else if (type1 instanceof ArrayType && type2 instanceof ArrayType) { 221 | return typesAreEqual(type1.elementType, type2.elementType); 222 | } else if (type1 instanceof FunctionType && type2 instanceof FunctionType) { 223 | if (type1.paramTypes.length !== type2.paramTypes.length) { 224 | return false; 225 | } 226 | for (let i = 0; i < type1.paramTypes.length; i++) { 227 | if (!typesAreEqual(type1.paramTypes[i], type2.paramTypes[i])) { 228 | return false; 229 | } 230 | } 231 | return typesAreEqual(type1.returnType, type2.returnType); 232 | } else if (type1 instanceof AdtType && type2 instanceof AdtType) { 233 | return type1.name === type2.name; 234 | } else if (type1 instanceof GenericType && type2 instanceof GenericType) { 235 | if ( 236 | type1.name !== type2.name || 237 | type1.typeArguments.length !== type2.typeArguments.length 238 | ) { 239 | // Allow comparison if one has no args (placeholder) - refinement might be needed 240 | if ( 241 | type1.typeArguments.length === 0 || 242 | type2.typeArguments.length === 0 243 | ) { 244 | return type1.name === type2.name; 245 | } 246 | return false; 247 | } 248 | for (let i = 0; i < type1.typeArguments.length; i++) { 249 | if (!typesAreEqual(type1.typeArguments[i], type2.typeArguments[i])) { 250 | return false; 251 | } 252 | } 253 | return true; 254 | } else if (type1 instanceof TypeVariable && type2 instanceof TypeVariable) { 255 | return type1.id === type2.id; // Compare by ID for uniqueness 256 | } else if (type1 instanceof JsxElementType && type2 instanceof JsxElementType) { 257 | // Two JsxElement types are equal if their expected props types are equal 258 | return typesAreEqual(type1.propsType, type2.propsType); 259 | } else if (type1 instanceof StringLiteralType && type2 instanceof StringLiteralType) { 260 | return type1.value === type2.value; 261 | } else if (type1 instanceof LiteralUnionType && type2 instanceof LiteralUnionType) { 262 | if (type1.values.size !== type2.values.size) { 263 | return false; 264 | } 265 | for (const val of type1.values) { 266 | if (!type2.values.has(val)) { 267 | return false; 268 | } 269 | } 270 | return true; 271 | } 272 | // Cross-type comparisons (e.g., StringLiteralType vs LiteralUnionType) are handled by unification, not equality. 273 | return false; 274 | } 275 | 276 | // Singleton instances for primitive types 277 | export const StringType = StringTypeClass.instance; 278 | export const NumberType = NumberTypeClass.instance; 279 | export const BooleanType = BooleanTypeClass.instance; 280 | export const UnitType = UnitTypeClass.instance; 281 | export const UnknownType = UnknownTypeClass.instance; 282 | 283 | // --- Specific ADT for CSS Display Property --- 284 | export const DisplayTypeAdt = new AdtType("DisplayType"); 285 | // --- End DisplayType ADT --- 286 | -------------------------------------------------------------------------------- /ChicoryVisitor.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { ParserRuleContext } from "antlr4ng"; 3 | import * as parser from "./generated/ChicoryParser"; 4 | import { ChicoryTypeChecker } from "./ChicoryTypeCheckerVisitor"; 5 | import { CompilationError, TypeHintWithContext, ChicoryType } from "./env"; 6 | import { 7 | AdtType, 8 | ArrayType, 9 | FunctionType, 10 | GenericType, 11 | RecordType, // Added import 12 | } from "./ChicoryTypes"; 13 | 14 | export class ChicoryParserVisitor { 15 | private filename: string; 16 | private typeChecker: ChicoryTypeChecker; 17 | private indentLevel: number = 0; 18 | private scopeLevel: number = 0; 19 | private uniqueVarCounter: number = 0; 20 | private errors: CompilationError[] = []; 21 | private hints: TypeHintWithContext[] = []; 22 | private expressionTypes: Map = new Map(); 23 | 24 | constructor(typeChecker: ChicoryTypeChecker, filename: string) { 25 | this.typeChecker = typeChecker || new ChicoryTypeChecker(); 26 | this.filename = filename; 27 | } 28 | 29 | // Utility to generate consistent indentation 30 | private indent(): string { 31 | return " ".repeat(this.indentLevel); 32 | } 33 | 34 | // Generate unique variable names per instance 35 | private getUniqueChicoryVariableName(): string { 36 | return `__chicory_var_${this.uniqueVarCounter++}`; 37 | } 38 | 39 | // Error reporting for LSP integration 40 | private reportError(message: string, context: ParserRuleContext): void { 41 | this.errors.push({ message, context }); 42 | } 43 | 44 | // Scope management 45 | private enterScope(): void { 46 | this.scopeLevel++; 47 | } 48 | 49 | private exitScope(): void { 50 | this.scopeLevel--; 51 | } 52 | 53 | // Main entry point for compilation 54 | visitProgram(ctx: parser.ProgramContext): string { 55 | const lines: string[] = ctx.stmt().map((stmt) => { 56 | try { 57 | const result = this.visitStmt(stmt); 58 | console.log(`[visitProgram] Visited stmt: ${stmt.getText().substring(0, 50)}... -> JS length: ${result?.length ?? 'undefined'}`); // LOGGING 59 | return result; 60 | } catch (e) { 61 | const errorMsg = e instanceof Error ? e.message : String(e); 62 | const stack = e instanceof Error ? e.stack : ''; 63 | console.error(`[visitProgram] ERROR visiting statement: ${stmt.getText().substring(0, 80)}...`); // LOGGING 64 | console.error(` Error: ${errorMsg}`); // LOGGING 65 | console.error(` Stack: ${stack}`); // LOGGING 66 | // Report a generic error associated with the statement context 67 | this.reportError(`Internal error during compilation of statement: ${errorMsg}`, stmt); 68 | return `/* ERROR visiting statement: ${stmt.getText().substring(0, 50)}... */`; // Return placeholder 69 | } 70 | }); 71 | if (ctx.exportStmt()) { 72 | try { // Also wrap export statement visit 73 | lines.push(this.visitExportStmt(ctx.exportStmt()!)); 74 | } catch (e) { 75 | const errorMsg = e instanceof Error ? e.message : String(e); 76 | console.error(`[visitProgram] ERROR visiting export statement: ${ctx.exportStmt()!.getText()}`); // LOGGING 77 | console.error(` Error: ${errorMsg}`); // LOGGING 78 | this.reportError(`Internal error during compilation of export statement: ${errorMsg}`, ctx.exportStmt()!); 79 | lines.push(`/* ERROR visiting export statement */`); 80 | } 81 | } 82 | return lines.join("\n"); 83 | } 84 | 85 | visitStmt(ctx: parser.StmtContext): string { 86 | let resultJs = ""; // Initialize 87 | if (ctx.assignStmt()) { 88 | resultJs = `${this.visitAssignStmt(ctx.assignStmt()!)};`; 89 | } else if (ctx.typeDefinition()) { 90 | resultJs = `${this.visitTypeDefinition(ctx.typeDefinition()!)}`; 91 | } else if (ctx.importStmt()) { 92 | resultJs = `${this.visitImportStmt(ctx.importStmt()!)};`; 93 | } else if (ctx.globalStmt()) { 94 | // We only need global statements to type check references to identifiers in the global scope 95 | // The identifiers compile 1:1 so we don't need to do anything here 96 | } else if (ctx.mutateStmt()) { 97 | resultJs = `${this.visitMutateStmt(ctx.mutateStmt()!)};`; 98 | } else if (ctx.expr()) { 99 | resultJs = `${this.visitExpr(ctx.expr()!)};`; 100 | } else { 101 | this.reportError(`Unknown statement type: ${ctx.getText()}`, ctx); 102 | resultJs = ""; // Continue processing with a no-op 103 | } 104 | // console.log(`[visitStmt] Input: ${ctx.getText().substring(0, 50)}... Output JS: ${resultJs}`); // LOGGING REMOVED 105 | return resultJs; 106 | } 107 | 108 | visitAssignStmt(ctx: parser.AssignStmtContext): string { 109 | const assignKwd = ctx.assignKwd().getText(); // 'let' or 'const' 110 | const targetCtx = ctx.assignTarget(); 111 | // Get the type associated with the *assignment target* by the type checker. 112 | // This should be the final, unified type considering annotations. 113 | const expectedType = this.expressionTypes.get(targetCtx); 114 | const expr = this.visitExpr(ctx.expr(), expectedType); // Pass expected type to RHS visitor 115 | 116 | let targetJs: string; 117 | if (targetCtx.IDENTIFIER()) { 118 | targetJs = targetCtx.IDENTIFIER()!.getText(); 119 | } else if (targetCtx.recordDestructuringPattern()) { 120 | const identifiers = targetCtx 121 | .recordDestructuringPattern()! 122 | .IDENTIFIER() 123 | .map((id) => id.getText()) 124 | .join(", "); 125 | targetJs = `{ ${identifiers} }`; 126 | } else if (targetCtx.arrayDestructuringPattern()) { 127 | const identifiers = targetCtx 128 | .arrayDestructuringPattern()! 129 | .IDENTIFIER() 130 | .map((id) => id.getText()) 131 | .join(", "); 132 | targetJs = `[ ${identifiers} ]`; 133 | } else { 134 | targetJs = `/* ERROR: Unknown assignment target */`; 135 | this.reportError( 136 | `Unknown assignment target type during compilation: ${targetCtx.getText()}`, 137 | targetCtx 138 | ); 139 | } 140 | return `${this.indent()}${assignKwd} ${targetJs} = ${expr}`; 141 | } 142 | 143 | visitMutateStmt(ctx: parser.MutateStmtContext): string { 144 | const baseIdentifierName = ctx.IDENTIFIER().getText(); 145 | let lhsJs = baseIdentifierName; 146 | 147 | const tailExprs = ctx.identifierTailExpr(); 148 | // The type checker already has the types for these expressions, 149 | // so we retrieve them to pass to compileTailExpr if needed. 150 | let currentLhsType = this.expressionTypes.get(ctx); 151 | 152 | for (const tail of tailExprs) { 153 | // In the compiler, we primarily construct the JS string. 154 | // The type of the base (lhsJs so far) might be needed if compileTailExpr 155 | // has different strategies based on type (e.g. for array index Option wrapping). 156 | // However, for simple mutation, direct JS translation is often enough. 157 | if (tail instanceof parser.IdentifierTailMemberExpressionContext) { 158 | lhsJs += `.${tail.IDENTIFIER().getText()}`; 159 | currentLhsType = this.expressionTypes.get(tail); 160 | } else if (tail instanceof parser.IdentifierTailIndexExpressionContext) { 161 | const indexExprJs = this.visitExpr(tail.expr()); 162 | lhsJs += `[${indexExprJs}]`; 163 | currentLhsType = this.expressionTypes.get(tail); 164 | } 165 | } 166 | 167 | const rhsJs = this.visitExpr(ctx.expr()); 168 | return `${this.indent()}${lhsJs} = ${rhsJs}`; 169 | } 170 | 171 | visitExportStmt(ctx: parser.ExportStmtContext): string { 172 | const identifiers = ctx 173 | .IDENTIFIER() 174 | .map((id) => id.getText()) 175 | .join(", "); 176 | return `${this.indent()}export { ${identifiers} };`; 177 | } 178 | 179 | visitTypeDefinition(ctx: parser.TypeDefinitionContext): string { 180 | const typeName = ctx.IDENTIFIER().getText(); 181 | const typeExpr = ctx.typeExpr().primaryTypeExpr(); // Keep reference for erasure check later 182 | 183 | // --- Generate ADT Constructors if they exist --- 184 | console.log(`[visitTypeDefinition] Checking for constructors for type '${typeName}'`); 185 | const allConstructors = this.typeChecker.getConstructors(); 186 | const constructors = allConstructors.filter((c) => c.adtName === typeName); 187 | console.log(`[visitTypeDefinition] Found ${constructors.length} constructors for ADT '${typeName}':`, constructors.map(c => c.name)); 188 | 189 | if (constructors.length > 0) { 190 | // Generate constructor functions for each ADT variant found 191 | const constructorFunctions = constructors 192 | .map((constructor) => { 193 | const constructorName = constructor.name; 194 | 195 | // Check if the constructor takes parameters 196 | const constructorType = constructor.type; 197 | if ( 198 | constructorType instanceof FunctionType && 199 | constructorType.paramTypes.length > 0 200 | ) { 201 | return `${this.indent()}const ${constructorName} = (value) => { return { type: "${constructorName}", value }; };`; 202 | } else { 203 | // For no-argument constructors, create the object directly 204 | return `${this.indent()}const ${constructorName} = () => { return { type: "${constructorName}" }; };`; 205 | } 206 | }) 207 | .join("\n"); 208 | // Return the generated functions. We don't erase ADTs. 209 | return constructorFunctions; 210 | } 211 | 212 | // --- If no constructors found, proceed with Type Erasure --- 213 | console.log(`[visitTypeDefinition] No constructors found for '${typeName}'. Performing type erasure.`); 214 | // Erase function types, generic type aliases (that aren't ADTs), and simple aliases 215 | // Use the original typeExpr reference to check the structure for erasure purposes 216 | if (typeExpr.functionType() || typeExpr.genericTypeExpr() || typeExpr.recordType() || typeExpr.tupleType() || typeExpr.primitiveType() || typeExpr.IDENTIFIER()) { 217 | return `${this.indent()}/* Type Erasure: ${typeName} */`; 218 | } 219 | 220 | // Fallback erasure (should ideally not be reached if grammar/logic is complete) 221 | console.warn(`[visitTypeDefinition] Unhandled type structure for erasure: ${typeExpr.getText()}`); 222 | return `${this.indent()}/* Type Erasure (Fallback): ${typeName} */`; 223 | } 224 | 225 | visitImportStmt(ctx: parser.ImportStmtContext): string { 226 | if ( 227 | !( 228 | ctx instanceof parser.ImportStatementContext || 229 | ctx instanceof parser.BindStatementContext 230 | ) 231 | ) { 232 | throw new Error("Invalid import statement"); 233 | } 234 | const fromPathRaw = ctx.STRING().getText(); 235 | let fromPath = fromPathRaw.substring(1, fromPathRaw.length - 1); 236 | 237 | // Check if it's a Chicory import based on the original path 238 | // const isChicoryImport = fromPath.endsWith(".chic"); 239 | // if (isChicoryImport) { 240 | // fromPath = fromPath.replace(/\.chic$/, ".js"); 241 | // } 242 | // const jsFromPath = `"${fromPath}"`; 243 | 244 | if (ctx instanceof parser.ImportStatementContext) { 245 | const defaultImport = ctx.IDENTIFIER() ? ctx.IDENTIFIER()!.getText() : ""; 246 | const destructuring = ctx.destructuringImportIdentifier() 247 | ? this.visitDestructuringImportIdentifier( 248 | ctx.destructuringImportIdentifier()! 249 | ) 250 | : ""; 251 | const body = [defaultImport, destructuring].filter(Boolean).join(", "); 252 | // return this.indent() + `import ${body} from ${jsFromPath}`; 253 | return this.indent() + `import ${body} from "${fromPath}"`; 254 | } else if (ctx instanceof parser.BindStatementContext) { 255 | const defaultImport = ctx.IDENTIFIER() ? ctx.IDENTIFIER()!.getText() : ""; 256 | const destructuring = ctx.bindingImportIdentifier() 257 | ? this.visitBindingImportIdentifier(ctx.bindingImportIdentifier()!) 258 | : ""; 259 | const body = [defaultImport, destructuring].filter(Boolean).join(", "); 260 | const from = ctx.STRING().getText(); 261 | return `${this.indent()}import ${body} from ${from}`; 262 | } 263 | 264 | throw new Error("Invalid import statement"); 265 | } 266 | 267 | visitDestructuringImportIdentifier( 268 | ctx: parser.DestructuringImportIdentifierContext 269 | ): string { 270 | const identifiers = ctx.IDENTIFIER(); 271 | return identifiers.length > 0 272 | ? `{ ${identifiers.map((id) => id.getText()).join(", ")} }` 273 | : ""; 274 | } 275 | 276 | visitBindingImportIdentifier( 277 | ctx: parser.BindingImportIdentifierContext 278 | ): string { 279 | const bindingIdentifiers = ctx 280 | .bindingIdentifier() 281 | .map((binding) => binding.IDENTIFIER().getText()); 282 | 283 | return bindingIdentifiers.length > 0 284 | ? `{ ${bindingIdentifiers.join(", ")} }` 285 | : ""; 286 | } 287 | 288 | // Add expectedType parameter 289 | visitExpr(ctx: parser.ExprContext, expectedType?: ChicoryType): string { // Added expectedType 290 | // Get the type of the primary expression from the map 291 | const primaryExprCtx = ctx.primaryExpr(); 292 | // Pass expectedType down to primary expression visitor 293 | let currentJs = this.visitPrimaryExpr(primaryExprCtx, expectedType); 294 | 295 | // --- Get Type of Base Expression --- 296 | // Use the specific context node that the type checker used as the key. 297 | // For identifiers, it's the IdentifierExpressionContext itself. 298 | // For other primary expressions (literals, records, etc.), it's the PrimaryExprContext. 299 | let typeLookupCtx: ParserRuleContext = primaryExprCtx; 300 | if (primaryExprCtx instanceof parser.IdentifierExpressionContext) { 301 | typeLookupCtx = primaryExprCtx; // Use the specific context for identifiers 302 | } 303 | // Add 'else if' for other specific contexts if needed (e.g., LiteralExpressionContext) 304 | let currentType = this.expressionTypes.get(typeLookupCtx); // Use the determined context for lookup 305 | // --- End Get Type --- 306 | 307 | 308 | // Iterate through tail expressions, compiling them sequentially 309 | for (const tailExprCtx of ctx.tailExpr()) { 310 | // Pass the current JS string and its inferred type to the compiler helper 311 | currentJs = this.compileTailExpr(tailExprCtx, currentJs, currentType); 312 | // Update the current type for the next iteration using the type map 313 | currentType = this.expressionTypes.get(tailExprCtx); 314 | if (!currentType) { 315 | // This might happen if type checking failed for this part. 316 | // Log or handle? For now, continue, JS might still be valid. 317 | console.warn(`No type found for tail expression: ${tailExprCtx.getText()}`); 318 | } 319 | } 320 | return currentJs; // Return the final compiled JS string 321 | } 322 | 323 | compileTailExpr( 324 | ctx: parser.TailExprContext, 325 | baseJs: string, 326 | baseType: ChicoryType | undefined 327 | ): string { 328 | // We use baseType (type of the expression *before* this tail part) 329 | // to decide *how* to compile this tail part. 330 | 331 | if (ctx.ruleContext instanceof parser.MemberExpressionContext) { 332 | const memberName = (ctx as parser.MemberExpressionContext) 333 | .IDENTIFIER() 334 | .getText(); 335 | // Direct JS member access works for records and arrays (for built-ins) 336 | return `${baseJs}.${memberName}`; 337 | } else if (ctx.ruleContext instanceof parser.IndexExpressionContext) { 338 | const indexExprJs = this.visitExpr( 339 | (ctx as parser.IndexExpressionContext).expr() 340 | ); 341 | // Use the type of the base expression to decide compilation strategy 342 | if (baseType instanceof ArrayType) { 343 | // Use the injected runtime helper for safe array access returning Option 344 | // NOTE: The type checker now handles Option wrapping for array index, 345 | // so the compiler just needs to generate the JS index access. 346 | // The type checker ensures the result is Option. 347 | // However, if we want runtime safety *in JS* (e.g., for out-of-bounds), 348 | // a helper might still be useful, but let's stick to direct access for now 349 | // and rely on the type checker's Option result type. 350 | // If a helper IS used, it must return the Some/None structure. 351 | // return `__chicory_array_index(${baseJs}, ${indexExprJs})`; // Example helper 352 | return `${baseJs}[${indexExprJs}]`; // Direct access (JS returns undefined if out of bounds) 353 | } else { 354 | // Assume Tuple access (or potentially String in future) 355 | // Type checker already validated this (or errored) 356 | return `${baseJs}[${indexExprJs}]`; 357 | } 358 | } else if (ctx.ruleContext instanceof parser.CallExpressionContext) { 359 | const callCtx = (ctx as parser.CallExpressionContext).callExpr(); 360 | // Determine expected parameter types directly from the FunctionType 361 | const expectedParamTypes = (baseType instanceof FunctionType) ? baseType.paramTypes : []; 362 | 363 | const args = callCtx.expr() 364 | ? callCtx 365 | .expr() 366 | .map((expr, index) => { 367 | const expectedArgType = expectedParamTypes[index]; // Get expected type for this arg 368 | // Pass expected type down to the argument expression visitor 369 | return this.visitExpr(expr, expectedArgType); 370 | }) 371 | .join(", ") 372 | : ""; 373 | 374 | // ---> START REPLACEMENT <--- // Note: This section handles Option wrapping for specific array methods 375 | 376 | // Get the type inferred by the type checker for the *result* of this specific call expression node 377 | const resultType = this.expressionTypes.get(ctx); // ctx is the TailExprContext wrapping CallExpressionContext 378 | 379 | // Check if the result type is Option<...> AND if the method being called is find or findIndex 380 | if (resultType instanceof GenericType && resultType.name === "Option") { 381 | // Check the actual method name stored in baseJs (e.g., "myArray.find") 382 | if (baseJs.endsWith(".find")) { 383 | return `((__res) => __res === undefined ? None() : Some(__res))(${baseJs}(${args}))`; 384 | } else if (baseJs.endsWith(".findIndex")) { 385 | return `((__res) => __res === -1 ? None() : Some(__res))(${baseJs}(${args}))`; 386 | } 387 | // If other Option-returning methods are added, handle them here. 388 | } 389 | 390 | // Default case: Not an Option-returning array method we handle specially, 391 | // or type information was missing. Generate standard function call syntax. 392 | return `${baseJs}(${args})`; 393 | 394 | // ---> END REPLACEMENT <--- 395 | } else if (ctx.ruleContext instanceof parser.OperationExpressionContext) { 396 | const op = (ctx as parser.OperationExpressionContext) 397 | .OPERATOR() 398 | .getText(); 399 | const rhsJs = this.visitExpr( 400 | (ctx as parser.OperationExpressionContext).expr() 401 | ); 402 | // Standard binary operation syntax 403 | return `${baseJs} ${op} ${rhsJs}`; 404 | } 405 | 406 | this.reportError( 407 | `Unknown tail expression type during compilation: ${ctx.getText()}`, 408 | ctx 409 | ); 410 | return `${baseJs}/* ERROR: Unknown tail expr ${ctx.getText()} */`; 411 | } 412 | 413 | visitTailExpr(ctx: parser.TailExprContext): string { 414 | if (ctx.ruleContext instanceof parser.MemberExpressionContext) { 415 | return this.visitMemberExpr(ctx as parser.MemberExpressionContext); 416 | } else if (ctx.ruleContext instanceof parser.IndexExpressionContext) { 417 | return this.visitIndexExpr(ctx as parser.IndexExpressionContext); 418 | } else if (ctx.ruleContext instanceof parser.CallExpressionContext) { 419 | return this.visitCallExpr( 420 | (ctx as parser.CallExpressionContext).callExpr() 421 | ); 422 | } else if (ctx.ruleContext instanceof parser.OperationExpressionContext) { 423 | return this.visitOperation(ctx as parser.OperationExpressionContext); 424 | } 425 | this.reportError(`Unknown tail expression type: ${ctx.getText()}`, ctx); 426 | return ""; 427 | } 428 | 429 | visitMemberExpr(ctx: parser.MemberExpressionContext): string { 430 | return `.${ctx.IDENTIFIER().getText()}`; 431 | } 432 | 433 | visitIndexExpr(ctx: parser.IndexExpressionContext): string { 434 | return `[${this.visitExpr(ctx.expr())}]`; 435 | } 436 | 437 | visitOperation(ctx: parser.OperationExpressionContext): string { 438 | return ` ${ctx.OPERATOR().getText()} ${this.visitExpr(ctx.expr())}`; 439 | } 440 | 441 | // Add expectedType parameter 442 | visitPrimaryExpr( 443 | ctx: parser.PrimaryExprContext, 444 | expectedType?: ChicoryType // Added expectedType 445 | ): string { 446 | const child = ctx.getChild(0); 447 | if (ctx instanceof parser.ParenExpressionContext) { 448 | // Pass expectedType through parentheses 449 | return `(${this.visitExpr(ctx.expr(), expectedType)})`; // Pass expectedType 450 | } else if (child instanceof parser.IfExprContext) { 451 | return this.visitIfElseExpr(child); // If doesn't directly use expectedType here 452 | } else if (child instanceof parser.FuncExprContext) { 453 | return this.visitFuncExpr(child); 454 | } else if (child instanceof parser.JsxExprContext) { 455 | return this.visitJsxExpr(child); 456 | } else if (child instanceof parser.MatchExprContext) { 457 | return this.visitMatchExpr(child); 458 | } else if (child instanceof parser.BlockExprContext) { 459 | return this.visitBlockExpr(child); 460 | } else if (child instanceof parser.RecordExprContext) { 461 | // Pass expectedType to record expression visitor 462 | return this.visitRecordExpr(child, expectedType); // Pass expectedType 463 | } else if (child instanceof parser.ArrayLikeExprContext) { 464 | return this.visitArrayLikeExpr(child); // Array doesn't use expectedType directly here 465 | } else if (ctx.ruleContext instanceof parser.IdentifierExpressionContext) { 466 | return this.visitIdentifier(ctx); // Identifier doesn't use expectedType directly here 467 | } else if (child instanceof parser.LiteralContext) { 468 | return this.visitLiteral(child); 469 | } 470 | this.reportError(`Unknown primary expression type: ${ctx.getText()}`, ctx); 471 | return ""; 472 | } 473 | 474 | visitIfElseExpr(ctx: parser.IfExprContext): string { 475 | const ifs = ctx.justIfExpr().map((justIf) => this.visitIfExpr(justIf)); 476 | const getElseExpr = () => { 477 | const child = ctx.expr()!.getChild(0); 478 | return child instanceof parser.BlockExpressionContext 479 | ? this.visitBlockExpr(child.blockExpr()) 480 | : `{ return ${this.visitExpr(ctx.expr()!)}; }`; 481 | }; 482 | return ( 483 | ifs.join("") + (ctx.expr() ? `(() => ${getElseExpr()})()` : "undefined") 484 | ); 485 | } 486 | 487 | visitIfExpr(ctx: parser.JustIfExprContext): string { 488 | const condition = this.visitExpr(ctx.expr()[0]); 489 | const thenExpr = ctx.expr()[1].getChild(0); 490 | const block = 491 | thenExpr instanceof parser.BlockExpressionContext 492 | ? this.visitBlockExpr(thenExpr.blockExpr()) 493 | : `{ return ${this.visitExpr(ctx.expr()[1])}; }`; 494 | return `(${condition}) ? (() => ${block})() : `; 495 | } 496 | 497 | visitFuncExpr(ctx: parser.FuncExprContext): string { 498 | this.enterScope(); 499 | 500 | const params: string[] = []; 501 | if ( 502 | ctx instanceof parser.ParenFunctionExpressionContext && 503 | ctx.parameterList() 504 | ) { 505 | params.push(...this.visitParameterList(ctx.parameterList()!)); 506 | } else if (ctx instanceof parser.ParenlessFunctionExpressionContext) { 507 | params.push(ctx.IDENTIFIER().getText()); 508 | } else if (ctx instanceof parser.ParenFunctionExpressionWildcardContext || ctx instanceof parser.ParenlessFunctionExpressionWildcardContext) { 509 | params.push("_"); 510 | } 511 | 512 | // @ts-expect-error TS can't tell that ctx will always have an expr. But we know it will because there are only two options and both have one expr. 513 | const body = this.visitExpr(ctx.expr()!); 514 | 515 | this.exitScope(); 516 | return `(${params.join(", ")}) => ${body}`; 517 | } 518 | 519 | visitParameterList(ctx: parser.ParameterListContext): string[] { 520 | return ctx.idOrWild().map((id) => id.getText()); 521 | } 522 | 523 | visitCallExpr(ctx: parser.CallExprContext): string { 524 | const args = ctx.expr() 525 | ? ctx 526 | .expr() 527 | .map((expr) => this.visitExpr(expr)) 528 | .join(", ") 529 | : ""; 530 | return `(${args})`; 531 | } 532 | 533 | visitMatchExpr(ctx: parser.MatchExprContext): string { 534 | this.indentLevel++; 535 | const expr = this.visitExpr(ctx.expr()); 536 | const varName = this.getUniqueChicoryVariableName(); 537 | const matchExpr = `${this.indent()}const ${varName} = ${expr};`; 538 | const arms = ctx 539 | .matchArm() 540 | .map( 541 | (arm, i) => 542 | `${this.indent()}${i > 0 ? "else " : ""}${this.visitMatchArm( 543 | arm, 544 | varName 545 | )}` 546 | ); 547 | const body = [matchExpr, ...arms].join("\n"); 548 | this.indentLevel--; 549 | return `(() => {\n${body}\n${this.indent()}})()`; 550 | } 551 | 552 | visitMatchArm(ctx: parser.MatchArmContext, varName: string): string { 553 | const { pattern, inject } = this.visitPattern(ctx.matchPattern(), varName); 554 | const getBlock = () => { 555 | const childExpr = ctx.expr().getChild(0); 556 | if (!childExpr) return ""; 557 | if (childExpr instanceof parser.BlockExpressionContext) { 558 | return this.visitBlockExpr(childExpr.blockExpr(), inject); 559 | } 560 | const expr = `return ${this.visitExpr(ctx.expr())};`; 561 | if (inject) { 562 | this.indentLevel++; 563 | const blockBody = `${this.indent()}${inject}\n${this.indent()}${expr}`; 564 | this.indentLevel--; 565 | return `{\n${blockBody}\n${this.indent()}}`; 566 | } 567 | return expr; 568 | }; 569 | return `if (${pattern}) ${getBlock()}`; 570 | } 571 | 572 | // We need tons of work here, we need to disambiguate identifiers that are adts vs destructuring identifiers 573 | visitPattern( 574 | ctx: parser.MatchPatternContext, 575 | varName: string 576 | ): { pattern: string; inject?: string } { 577 | if (ctx.ruleContext instanceof parser.BareAdtOrVariableMatchPatternContext) { 578 | const adtName = (ctx as parser.BareAdtOrVariableMatchPatternContext) 579 | .IDENTIFIER() 580 | .getText(); 581 | return { pattern: `${varName}.type === "${adtName}"` }; 582 | } else if ( 583 | ctx.ruleContext instanceof parser.AdtWithParamMatchPatternContext 584 | ) { 585 | const [adtName, paramName] = ( 586 | ctx as parser.AdtWithParamMatchPatternContext 587 | ) 588 | .IDENTIFIER() 589 | .map((id) => id.getText()); 590 | return { 591 | pattern: `${varName}.type === "${adtName}"`, 592 | inject: `const ${paramName} = ${varName}.value;`, 593 | }; 594 | } else if ( 595 | ctx.ruleContext instanceof parser.AdtWithWildcardMatchPatternContext 596 | ) { 597 | const adtName = (ctx as parser.AdtWithWildcardMatchPatternContext) 598 | .IDENTIFIER() 599 | .getText(); 600 | // Just check the type 601 | return { pattern: `${varName}.type === "${adtName}"` }; 602 | } else if ( 603 | ctx.ruleContext instanceof parser.AdtWithLiteralMatchPatternContext 604 | ) { 605 | const adtName = (ctx as parser.AdtWithLiteralMatchPatternContext) 606 | .IDENTIFIER() 607 | .getText(); 608 | const literalValue = this.visitLiteral( 609 | (ctx as parser.AdtWithLiteralMatchPatternContext).literal() 610 | ); 611 | return { 612 | pattern: `${varName}.type === "${adtName}" && ${varName}.value === ${literalValue}`, 613 | }; 614 | } else if (ctx.ruleContext instanceof parser.WildcardMatchPatternContext) { 615 | return { pattern: "true" }; 616 | } else if (ctx.ruleContext instanceof parser.LiteralMatchPatternContext) { 617 | const literalValue = this.visitLiteral( 618 | (ctx as parser.LiteralMatchPatternContext).literal() 619 | ); 620 | return { pattern: `${varName} === ${literalValue}` }; 621 | } 622 | this.reportError(`Unknown match pattern type: ${ctx.getText()}`, ctx); 623 | return { pattern: "false" }; 624 | } 625 | 626 | visitBlockExpr(ctx: parser.BlockExprContext, inject: string = ""): string { 627 | this.enterScope(); 628 | this.indentLevel++; 629 | 630 | const stmts = ctx.stmt() 631 | 632 | // If the final stmt is an expression, we need to "return" it 633 | // Otherwise it is a UnitType block 634 | const visitedStmts = stmts.at(-1)?.expr() 635 | ? [ 636 | ...stmts.slice(0, -1).map((stmt) => this.visitStmt(stmt)), 637 | `${this.indent()}return ${this.visitExpr(stmts.at(-1)!.expr()!)};` // Visit the last expression and return it 638 | ] 639 | : stmts.map((stmt) => this.visitStmt(stmt)); 640 | 641 | const block = [ 642 | ...(inject ? [this.indent() + inject] : []), 643 | ...visitedStmts, 644 | ]; 645 | this.indentLevel--; 646 | this.exitScope(); 647 | return `{\n${block.join("\n")}\n${this.indent()}}`; 648 | } 649 | 650 | // Add expectedType parameter 651 | visitRecordExpr( 652 | ctx: parser.RecordExprContext, 653 | expectedType?: ChicoryType 654 | ): string { 655 | const providedKeys = new Set(); 656 | const compiledKvs: string[] = []; 657 | 658 | // Compile provided key-value pairs 659 | ctx.recordKvExpr().forEach((kv) => { 660 | const key = kv.IDENTIFIER().getText(); 661 | providedKeys.add(key); 662 | compiledKvs.push(this.visitRecordKvExpr(kv)); 663 | }); 664 | 665 | // --- NEW Optional Field Handling (Rescript Style) --- 666 | // Use the *expected* type passed down from the calling context (assignment, function arg, etc.) 667 | // const expectedRecordType = this.expressionTypes.get(ctx); // <<< OLD: Used inferred type of the literal itself 668 | const expectedRecordType = expectedType; // <<< NEW: Use the passed-down expected type 669 | let finalRecordType: RecordType | null = null; 670 | 671 | if (expectedRecordType) { 672 | // Resolve potential aliases to get the underlying RecordType 673 | finalRecordType = this.typeChecker.resolveToRecordType(expectedRecordType); 674 | console.log(`[visitRecordExpr] Expected type (from param/annotation) resolved to: ${finalRecordType?.toString()}`); // Adjusted log 675 | } else { 676 | console.log(`[visitRecordExpr] No expected type passed down for record literal.`); // Adjusted log 677 | // If no expected type, compile literally (no Some/None wrapping) 678 | // This happens for inferred records: let r = { a: 1 } 679 | } 680 | 681 | if (finalRecordType) { 682 | // Expected type IS a RecordType, apply wrapping logic 683 | const finalCompiledKvs: string[] = []; 684 | 685 | // Process provided fields 686 | ctx.recordKvExpr().forEach((kv) => { 687 | const key = kv.IDENTIFIER().getText(); 688 | const valueJs = this.visitExpr(kv.expr()); // Compile the value expression 689 | const expectedFieldInfo = finalRecordType!.fields.get(key); 690 | const valueExprCtx = kv.expr(); // Get the original expression context for the value 691 | 692 | if (expectedFieldInfo?.optional) { 693 | // Check if the original expression was already Some(...) or None 694 | const valueText = valueExprCtx.getText(); 695 | const alreadyWrapped = valueText.startsWith("Some(") || valueText === "None"; // Basic check 696 | 697 | if (alreadyWrapped) { 698 | // If already wrapped in source, compile it directly without adding another Some() 699 | finalCompiledKvs.push(`${key}: ${valueJs}`); 700 | this.typeChecker.prelude.requireOptionType(); // Still need prelude 701 | console.log(` > Compiling optional field '${key}': Value already wrapped ('${valueText}'). Using direct value.`); 702 | } else { 703 | // If not already wrapped, wrap the compiled value in Some() 704 | finalCompiledKvs.push(`${key}: Some(${valueJs})`); 705 | this.typeChecker.prelude.requireOptionType(); // Ensure Some/None are available 706 | console.log(` > Compiling optional field '${key}': Wrapping value in Some().`); 707 | } 708 | } else { 709 | // If the expected field is required, compile directly 710 | finalCompiledKvs.push(`${key}: ${valueJs}`); 711 | if (!expectedFieldInfo) { 712 | console.warn(` > Field '${key}' provided but not found in expected type ${finalRecordType}. Compiling literally.`); 713 | } else { 714 | console.log(` > Compiling required field '${key}': Using direct value.`); 715 | } 716 | } 717 | providedKeys.add(key); // Track provided keys 718 | }); 719 | 720 | // Add None() for omitted optional fields 721 | for (const [expectedKey, expectedFieldInfo] of finalRecordType.fields) { 722 | if (!providedKeys.has(expectedKey) && expectedFieldInfo.optional) { 723 | finalCompiledKvs.push(`${expectedKey}: None()`); 724 | this.typeChecker.prelude.requireOptionType(); 725 | console.log(` > Compiling omitted optional field '${expectedKey}': Adding None().`); 726 | } 727 | // Omitted required fields are type errors, already handled by checker. 728 | } 729 | return `{ ${finalCompiledKvs.join(", ")} }`; 730 | 731 | } else { 732 | // No expected type or not a record type, compile literally 733 | console.log(`[visitRecordExpr] Compiling record literally (no expected type or not a record).`); 734 | return `{ ${compiledKvs.join(", ")} }`; 735 | } 736 | // --- End NEW Optional Field Handling --- 737 | } 738 | 739 | visitRecordKvExpr(ctx: parser.RecordKvExprContext): string { 740 | const key = ctx.IDENTIFIER().getText(); 741 | const value = this.visitExpr(ctx.expr()); 742 | return `${key}: ${value}`; 743 | } 744 | 745 | visitArrayLikeExpr(ctx: parser.ArrayLikeExprContext): string { 746 | const elements = ctx.expr().map((expr) => this.visitExpr(expr)); 747 | return `[${elements.join(", ")}]`; 748 | } 749 | 750 | visitJsxExpr(ctx: parser.JsxExprContext): string { 751 | if (ctx.jsxSelfClosingElement()) { 752 | return this.visitJsxSelfClosingElement(ctx.jsxSelfClosingElement()!); 753 | } 754 | const opening = this.visitJsxOpeningElement(ctx.jsxOpeningElement()!); 755 | const children = ctx 756 | .jsxChild() 757 | .map((child) => this.visitJsxChild(child)) 758 | .join(""); 759 | const closing = this.visitJsxClosingElement(ctx.jsxClosingElement()!); 760 | return `${opening}${children}${closing}`; 761 | } 762 | 763 | visitJsxSelfClosingElement(ctx: parser.JsxSelfClosingElementContext): string { 764 | const tag = ctx.IDENTIFIER().getText(); 765 | const attrs = ctx.jsxAttributes() 766 | ? this.visitJsxAttributes(ctx.jsxAttributes()!) 767 | : ""; 768 | return `${this.indent()}<${tag}${attrs} />`; 769 | } 770 | 771 | visitJsxOpeningElement(ctx: parser.JsxOpeningElementContext): string { 772 | const tag = ctx.IDENTIFIER().getText(); 773 | const attrs = ctx.jsxAttributes() 774 | ? this.visitJsxAttributes(ctx.jsxAttributes()!) 775 | : ""; 776 | return `${this.indent()}<${tag}${attrs}>`; 777 | } 778 | 779 | visitJsxClosingElement(ctx: parser.JsxClosingElementContext): string { 780 | const tag = ctx.IDENTIFIER().getText(); 781 | return `${this.indent()}`; 782 | } 783 | 784 | visitJsxAttributes(ctx: parser.JsxAttributesContext): string { 785 | return ctx 786 | .jsxAttribute() 787 | .map((attr) => this.visitJsxAttribute(attr)) 788 | .join(""); 789 | } 790 | 791 | visitJsxAttribute(ctx: parser.JsxAttributeContext): string { 792 | const name = ctx.IDENTIFIER()?.getText() || "type" 793 | const value = ctx.jsxAttributeValue() 794 | ? this.visitJsxAttributeValue(ctx.jsxAttributeValue()) 795 | : ""; 796 | return ` ${name}=${value}`; 797 | } 798 | 799 | visitJsxAttributeValue(ctx: parser.JsxAttributeValueContext): string { 800 | const text = ctx.getText(); 801 | // Preserve quotes for string literals, quote non-quoted values 802 | return text.startsWith('"') || text.startsWith("{") ? text : `"${text}"`; 803 | } 804 | 805 | visitJsxChild(ctx: parser.JsxChildContext): string { 806 | if (ctx instanceof parser.JsxChildJsxContext) { 807 | return this.visitJsxExpr(ctx.jsxExpr()); 808 | } else if (ctx instanceof parser.JsxChildExpressionContext) { 809 | return `{${this.visitExpr(ctx.expr())}}`; 810 | } else if (ctx instanceof parser.JsxChildTextContext) { 811 | return ctx.getText().trim(); 812 | } 813 | this.reportError(`Unknown JSX child type: ${ctx.getText()}`, ctx); 814 | return ""; 815 | } 816 | 817 | visitIdentifier(ctx: ParserRuleContext): string { 818 | const name = ctx.getText(); 819 | 820 | // Check if this is a no-argument ADT constructor 821 | const constructor = this.typeChecker 822 | .getConstructors() 823 | .find((c) => c.name === name); 824 | if (constructor) { 825 | const constructorType = constructor.type; 826 | if ( 827 | constructorType instanceof FunctionType && 828 | constructorType.paramTypes.length === 0 829 | ) { 830 | // For no-argument constructors, return the object directly 831 | return `{ type: "${name}" }`; 832 | } 833 | } 834 | 835 | return name; 836 | } 837 | 838 | visitLiteral(ctx: parser.LiteralContext): string { 839 | return ctx.getText(); 840 | } 841 | 842 | // Public method to get compilation output and errors 843 | getOutput(ctx: parser.ProgramContext): { 844 | code: string; 845 | errors: CompilationError[]; 846 | hints: { context: ParserRuleContext; type: string }[]; 847 | } { 848 | this.errors = []; // Reset errors per compilation 849 | this.hints = []; // Reset hints per compilation 850 | this.uniqueVarCounter = 0; // Reset variable counter 851 | this.scopeLevel = 0; // Reset scope level 852 | this.expressionTypes.clear(); // Clear type map 853 | 854 | const { errors, hints, expressionTypes, prelude } = 855 | this.typeChecker.check( 856 | ctx, 857 | this.filename, 858 | (fp) => readFileSync(fp, "utf-8"), 859 | new Map(), // Use actual cache/processing set if integrating properly 860 | new Set() 861 | ); 862 | this.expressionTypes = expressionTypes; 863 | // Store the prelude instance returned by the checker 864 | this.typeChecker.prelude = prelude; // Ensure the visitor uses the final prelude state 865 | 866 | const typeErrors = errors.map((err) => ({ 867 | message: `Type Error: ${err.message}`, 868 | context: err.context, 869 | })); 870 | 871 | // Collect type hints 872 | this.hints = hints; 873 | 874 | const compiledUserCode = this.visitProgram(ctx); 875 | console.log(`[getOutput] Compiled User Code:\n---\n${compiledUserCode}\n---`); // LOGGING 876 | 877 | // Use the prelude instance stored in the visitor's typeChecker instance 878 | const preludeCode = this.typeChecker.prelude.getPrelude(); // LOGGING 879 | console.log(`[getOutput] Prelude Code:\n---\n${preludeCode}\n---`); // LOGGING 880 | 881 | const finalCode = (preludeCode + "\n" + compiledUserCode).trim(); 882 | console.log(`[getOutput] Final Code:\n---\n${finalCode}\n---`); // LOGGING 883 | 884 | return { 885 | code: finalCode, 886 | errors: [...this.errors, ...typeErrors], 887 | hints: this.hints, 888 | }; 889 | } 890 | } 891 | -------------------------------------------------------------------------------- /Prelude.ts: -------------------------------------------------------------------------------- 1 | const preludeHelpers = { 2 | arrayIndex: ` 3 | const __chicory_array_index = (arr, index) => { 4 | // Basic bounds check + undefined check for safety 5 | const val = arr[index]; 6 | return (index >= 0 && index < arr.length && val !== undefined) ? Some(val) : None(); 7 | };`, 8 | optionType: ` 9 | const Some = (value) => ({ type: "Some", value }); 10 | const None = () => ({ type: "None" });`, 11 | resultType: ` 12 | const Ok = (value) => ({ type: "Ok", value }); 13 | const Err = (value) => ({ type: "Err", value });`, 14 | } 15 | 16 | export class Prelude { 17 | private arrayIndex = false 18 | private optionType = false 19 | private resultType = false 20 | 21 | constructor() {} 22 | 23 | requireArrayIndex() { 24 | this.arrayIndex = true 25 | } 26 | requireOptionType() { 27 | this.optionType = true 28 | } 29 | requireResultType() { 30 | this.resultType = true 31 | } 32 | 33 | getPrelude() { 34 | let prelude = "" 35 | if (this.arrayIndex) { 36 | prelude += preludeHelpers.arrayIndex 37 | } 38 | if (this.optionType) { 39 | prelude += preludeHelpers.optionType 40 | } 41 | if (this.resultType) { 42 | prelude += preludeHelpers.resultType 43 | } 44 | return prelude.trim() 45 | } 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐣 Chicory 2 | 3 | > caffeine-free javascript 4 | 5 | Chicory is a functional-friendly type-safe Javascript alternative that compiles to JSX. 6 | 7 | ```chicory 8 | import { document } from "bindings/browser" 9 | import ReactDOM from "bindings/react-dom" 10 | 11 | const Hello = ({ name }) => 12 | match (name) { 13 | "world" =>

Hello, WORLD!

, 14 | _ =>

Hello, {name}!

, 15 | } 16 | 17 | ReactDOM.render( 18 | , 19 | document.getElementById("root") 20 | ) 21 | ``` 22 | 23 | This is a WIP and a PoC. Many of the features you see above are implemented. But there is tons still to do, and that will just be to see if the idea is worth pursuing (and has traction in the wider community). 24 | 25 | 26 | ## About 27 | 28 | ### Why 29 | 30 | A "type-safe alternative to JS" sounds a lot like "typescript". If you've ever refactored a JS project, you probably wished you were using TS. But if you've ever refactored a TS project, you know that it's not as safe as you would like. Not only do `any` and `as` litter your codebase, but `null` and `undefined` still flow through the type system. Chicory aims to be a better alternative to both JS and TS. It's what TS could have been if it didn't try to support all of JS. 31 | 32 | Chicory is not as extensive as TS and doesn't aim to support all of JS. Instead, it aims to be familiar to JS developers so that there's an easy onramp. But all the footguns are gone, so you know your code will work if it compiles. Because it compiles to JSX, you can use your build system, libraries and tools, and you can run it on your favorite JS runtime. 33 | 34 | ### Features 35 | 36 | - If expressions 37 | - Match expressions for pattern matching 38 | - Algebraic data types 39 | - JSX support and compiles to JSX 40 | 41 | ### Goals 42 | 43 | - Performant JS 44 | - Readable compiled JSX (? maybe this is not a goal) 45 | - Easy bindings to JS libraries 46 | - JS FFI? 47 | 48 | ## Usage 49 | 50 | ### NPM 51 | 52 | You can use the compiler by downloading it from npm. 53 | 54 | ``` 55 | npm install @chicory-lang/compiler 56 | ``` 57 | 58 | Then you can use it in your code like this: 59 | 60 | ``` 61 | import { compile } from '@chicory-lang/compiler'; 62 | 63 | const code = ` 64 | const a = { 65 | const x = 1 66 | const y = 2 67 | const z = x + y 68 | z 69 | } 70 | `; 71 | 72 | const result = compile(code); 73 | 74 | console.log(result); 75 | ``` 76 | 77 | `result` will be an object with the following properties: 78 | 79 | - `code`: The compiled JSX code 80 | - `errors`: An array of errors that occurred during compilation 81 | - `hints`: An array of type hints generated during compilation 82 | 83 | 84 | ### Development 85 | 86 | The compiler currently runs in TS and is being developed with the Bun JS runtime (because it's fast, supports TS, and has built in testing). If you would like to contribute towards compiler, development, you will need to set up Bun first. 87 | 88 | #### Building 89 | 90 | To regenerate ANTLR4 files from the grammar and run tests: 91 | 92 | ``` 93 | bun run build 94 | ``` 95 | 96 | #### Executing 97 | 98 | If you have a `.chic` file that you would like to try to execute, you can use the `exec` helper function. This function will attempt to compile and run your code. 99 | 100 | ``` 101 | bun run exec ./sample.chic 102 | ``` 103 | 104 | **Note**: Right now errors from the type-checker while compiling will not prevent this script from trying to run your code. This means that JS interop is super easy ;) (but there's not type-checking going on). 105 | 106 | ### Running the Compiler 107 | 108 | ## TODO 109 | 110 | - [x] Vite plugin (wip) (https://github.com/chicory-lang/vite-plugin-chicory) 111 | - [ ] Documentation (wip) (https://chicory-lang.github.io/) 112 | - [ ] Language features & stabilization 113 | - [x] Hindley-Milner type inference (wip) 114 | - [ ] Bindings to JS libraries and runtimes 115 | - [x] Type checking (wip) 116 | - [x] Syntax highlighting 117 | - [x] Tree-sitter (wip) (https://github.com/chicory-lang/tree-sitter-chicory) 118 | - [x] Textmate (wip) (see https://github.com/chicory-lang/vscode-lsp-extension/tree/main/client/syntaxes) 119 | -------------------------------------------------------------------------------- /TypeEnvironment.ts: -------------------------------------------------------------------------------- 1 | import { ParserRuleContext } from "antlr4ng"; 2 | import { ChicoryType } from "./env"; 3 | 4 | export class TypeEnvironment { 5 | private bindings: Map; 6 | 7 | constructor(public parent: TypeEnvironment | null) { 8 | this.bindings = new Map(); 9 | } 10 | 11 | getType(identifier: string): ChicoryType | undefined { 12 | let type = this.bindings.get(identifier); 13 | if (type) { 14 | return type; 15 | } 16 | if (this.parent) { 17 | return this.parent.getType(identifier); 18 | } 19 | return undefined; 20 | } 21 | 22 | declare( 23 | identifier: string, 24 | type: ChicoryType, 25 | context: ParserRuleContext | null, 26 | pushError: (str) => void 27 | ): void { 28 | if (this.bindings.has(identifier)) { 29 | pushError( 30 | `Identifier '${identifier}' is already declared in this scope.` 31 | ); 32 | return; // We don't want to continue because this is an error 33 | } 34 | this.bindings.set(identifier, type); 35 | } 36 | 37 | pushScope(): TypeEnvironment { 38 | return new TypeEnvironment(this); 39 | } 40 | 41 | popScope(): TypeEnvironment { 42 | if (this.parent === null) { 43 | throw new Error("Cannot pop the global scope."); 44 | } 45 | return this.parent; 46 | } 47 | 48 | getAllTypes(): Map { 49 | return new Map(this.bindings); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | preload = ["./chicPlugin.ts"] 2 | -------------------------------------------------------------------------------- /chicPlugin.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "bun"; 2 | import compile from "./compile"; 3 | 4 | plugin({ 5 | name: "Chicory", 6 | async setup(build) { 7 | build.onLoad({ filter: /\.chic/ }, async (args) => { 8 | const file = await Bun.file(args.path).text(); 9 | const { code } = compile(file); 10 | return { 11 | exports: await import(`data:text/javascript,${encodeURIComponent(code)}`), 12 | loader: 'object', 13 | }; 14 | }) 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /compile.ts: -------------------------------------------------------------------------------- 1 | import { CharStream, CommonTokenStream, ParserRuleContext, ParseTreeWalker, TokenStream } from 'antlr4ng'; 2 | import { ChicoryLexer } from './generated/ChicoryLexer'; 3 | import { ChicoryParser } from './generated/ChicoryParser'; 4 | import { ChicoryParserVisitor } from './ChicoryVisitor'; 5 | import { ChicoryTypeChecker } from './ChicoryTypeCheckerVisitor'; 6 | import { LspDiagnostic, CompilationError, SyntaxError, TypeHint, LspRange } from './env'; 7 | import { ChicoryErrorListener } from './ChicoryErrorListener'; 8 | 9 | const rangeContains = (outer: LspRange, inner: LspRange): boolean => { 10 | return ( 11 | (outer.start.line < inner.start.line || 12 | (outer.start.line === inner.start.line && outer.start.character < inner.start.character)) 13 | && 14 | (outer.end.line > inner.end.line || 15 | (outer.end.line === inner.end.line && outer.end.character > inner.end.character)) 16 | ); 17 | } 18 | 19 | const filterOutErrorsThatContainOtherErrors = (errors: LspDiagnostic[]): LspDiagnostic[] => { 20 | const filteredErrors: LspDiagnostic[] = []; 21 | 22 | for (const error of errors) { 23 | let containsOtherError = false; 24 | 25 | for (const otherError of errors) { 26 | if (error !== otherError && rangeContains(error.range, otherError.range)) { 27 | containsOtherError = true 28 | } 29 | } 30 | if (!containsOtherError) { 31 | filteredErrors.push(error); 32 | } 33 | } 34 | 35 | return filteredErrors; 36 | } 37 | 38 | const getRange = (ctx: ParserRuleContext, tokenStream: TokenStream) => { 39 | const {start, stop} = ctx.getSourceInterval() 40 | const startToken = tokenStream.get(start) 41 | const stopToken = tokenStream.get(stop) 42 | return { 43 | start: { line: startToken.line - 1, character: startToken.column }, 44 | end: { line: stopToken.line - 1, character: stopToken.column + (stopToken.text?.length || 1) } 45 | } 46 | } 47 | 48 | const compilerErrorToLspError = (tokenStream: TokenStream) => ((e: CompilationError) => ({ 49 | severity: 1, // 1 is error 50 | message: e.message as string, 51 | range: getRange(e.context, tokenStream), 52 | source: "chicory", 53 | })) 54 | 55 | const syntaxErrorToLspError = (e: SyntaxError) => ({ 56 | severity: 1, // 1 is error 57 | source: "chicory", 58 | ...e 59 | }) 60 | 61 | export type CompileResult = { 62 | code: string; 63 | errors: LspDiagnostic[]; 64 | hints: TypeHint[]; 65 | } 66 | 67 | export default (source: string, path?: string): CompileResult => { 68 | if (!source.trim()) { 69 | return { code: "", errors: [], hints: [] } 70 | } 71 | let inputStream = CharStream.fromString(source); 72 | let lexer = new ChicoryLexer(inputStream); 73 | let tokenStream = new CommonTokenStream(lexer); 74 | let parser = new ChicoryParser(tokenStream); 75 | 76 | const errorListener = new ChicoryErrorListener(); 77 | lexer.removeErrorListeners(); 78 | lexer.addErrorListener(errorListener); 79 | parser.removeErrorListeners(); 80 | parser.addErrorListener(errorListener); 81 | 82 | let tree = parser.program(); 83 | 84 | // Create type checker first 85 | const typeChecker = new ChicoryTypeChecker(); 86 | 87 | // Create visitor with the type checker 88 | const visitor = new ChicoryParserVisitor(typeChecker, path || "__main__"); 89 | 90 | let code: string = "" 91 | let errors: LspDiagnostic[] = [] 92 | let hints: TypeHint[] = [] 93 | try { 94 | const {code: compiledCode, errors: unprocessedErrors, hints: unprocessedHints} = visitor.getOutput(tree) || {code: "", errors: [], hints: []} 95 | const mapErrors = compilerErrorToLspError(tokenStream) 96 | 97 | code = compiledCode 98 | errors.push(...unprocessedErrors.map(mapErrors)) 99 | hints = unprocessedHints.map(({context, type}) => ({ 100 | range: getRange(context, tokenStream), 101 | type 102 | })); 103 | } 104 | catch (e) { 105 | // We ensure that the compiler does not crash if the parser fails to produce a parseable parse tree 106 | } 107 | 108 | const syntaxErrors = errorListener.getErrors(); 109 | errors.push(...syntaxErrors.map(syntaxErrorToLspError)) 110 | 111 | errors = filterOutErrorsThatContainOtherErrors(errors) 112 | 113 | // Convert hints to LSP format 114 | 115 | return { 116 | code, 117 | errors, 118 | hints 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | import { ParserRuleContext } from "antlr4ng"; 2 | 3 | export interface ChicoryType { 4 | toString(): string; // For easy debugging and hint display 5 | } 6 | 7 | type CompilationError = { message: string; context: ParserRuleContext }; 8 | 9 | type SyntaxError = { message: string; range: LspRange }; 10 | 11 | type LspRange = { 12 | start: { 13 | line: number; 14 | character: number; 15 | }; 16 | end: { 17 | line: number; 18 | character: number; 19 | }; 20 | }; 21 | 22 | type LspDiagnostic = { 23 | severity: number; 24 | message: string; 25 | range: LspRange; 26 | source: string; 27 | }; 28 | 29 | type TypeHintWithContext = { 30 | context: ParserRuleContext; 31 | type: string; 32 | // message?: string; // Optional extra message 33 | }; 34 | 35 | type TypeHint = { 36 | range: LspRange; 37 | type: string; 38 | }; 39 | 40 | // Define the cache structure 41 | type CompilationCacheEntry = { 42 | exports: Map; 43 | errors: CompilationError[]; 44 | // Potentially add other artifacts like generated prelude requirements 45 | }; 46 | 47 | type CompilationCache = Map; // Key: absolute file path 48 | 49 | type ProcessingFiles = Set; // Key: absolute file path 50 | 51 | type SubstitutionMap = Map; 52 | -------------------------------------------------------------------------------- /execute.ts: -------------------------------------------------------------------------------- 1 | import compile from "./compile"; 2 | 3 | const yellowBoldTermPrefix = "\x1b[33m\x1b[1m"; 4 | const resetStyle = "\x1b[0m"; 5 | const yellow = (str: string) => yellowBoldTermPrefix + str + resetStyle; 6 | 7 | const usageInstructions = ` 8 | Usage: 9 | chicory [options] [file] 10 | Options: 11 | --compile / -c Compile the file and print the compiled code 12 | --hints Also print type hints (must be used with --compile) 13 | --help / -h Print this help message 14 | --version Print the version number 15 | File: 16 | The file to compile/exec 17 | ` 18 | 19 | const options = Bun.argv.slice(2); 20 | const filePath = options[options.length - 1]; 21 | 22 | if (options.includes("--help") || options.includes("-h")) { 23 | console.log(usageInstructions); 24 | process.exit(0); 25 | } 26 | 27 | if (options.includes("--version") || options.includes("-v")) { 28 | // read from package.json 29 | const packageJson = await Bun.file("package.json").json(); 30 | console.log(packageJson.version); 31 | process.exit(0); 32 | } 33 | 34 | if (!filePath) { 35 | console.error("No file specified"); 36 | process.exit(1); 37 | } 38 | 39 | const file = Bun.file(filePath); 40 | const source = await file.text(); 41 | 42 | console.log(yellow(" ⚡ Compiling Chicory Source ⚡")); 43 | const { code, errors, hints } = compile(source, filePath) || { code: "", errors: [] }; 44 | 45 | if (errors.length > 0) { 46 | console.error(errors.map((error, index) => 47 | `Error ${index}:\n${JSON.stringify(error.range.start)}\n${ 48 | JSON.stringify(error.message) 49 | }` 50 | ).join("\n---\n")); 51 | } 52 | 53 | // if "--compile" flag is passed, print the compiled code and exit: 54 | if (options.includes("--compile") || options.includes("-c")) { 55 | if (options.includes("--hints") && hints.length > 0) { 56 | console.log(yellow(" ⚡ Hints ⚡")); 57 | console.log(JSON.stringify(hints, null, 4)); 58 | } 59 | 60 | console.log(yellow(" ⚡ Compiled Code ⚡")); 61 | console.log(code); 62 | process.exit(0); 63 | } 64 | 65 | console.log(yellow(" ⚡ Executing ⚡")); 66 | 67 | // run the compiled code: 68 | const proc = Bun.spawn(["bun", "run", "-"], { 69 | stdin: "pipe", 70 | stdout: "inherit", 71 | }); 72 | proc.stdin.write(code); 73 | proc.stdin.end(); 74 | -------------------------------------------------------------------------------- /generate-chicory-dom-types.ts: -------------------------------------------------------------------------------- 1 | // To create: bun run this-file.ts > generated/internalChicoryJsxTypes.js 2 | import webCss from '@webref/css'; 3 | import webElements from '@webref/elements'; 4 | import webIdl from '@webref/idl'; 5 | // import webEvents from '@webref/events'; // Event data is indirectly used via IDL for event handler signatures 6 | 7 | // --- Helper to convert CSS property name to JS camelCase (for style object keys) --- 8 | function cssToJsName(cssName: string): string { 9 | return cssName.replace(/-([a-zA-Z0-9])/g, (g) => g[1].toUpperCase()); 10 | } 11 | 12 | // --- Helper to map basic CSS syntaxes to Chicory type strings --- 13 | function mapCssSyntaxToChicoryTypeString(syntax: string, propertyName: string): string { 14 | if (['color', 'background-color', 'border-color', 'outline-color'].includes(propertyName)) { 15 | return "StringType"; 16 | } 17 | if (syntax.includes('') || syntax.includes('') || syntax.includes('