diff --git a/.gitignore b/.gitignore index 3e22129..fe8a44b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/dist \ No newline at end of file +/dist +/target +/node_modules +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8de6439 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "funkylang" +version = "0.1.0" +dependencies = [ + "ena", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" diff --git a/src/ty.rs b/src/ty.rs index b5a4bb8..8dd6440 100644 --- a/src/ty.rs +++ b/src/ty.rs @@ -246,17 +246,17 @@ impl UnifyTable { .map_err(|(got, expected)| UnifyError::Mismatch(got, expected)) } - // NumVar ← 非数値型: エラー (Bool 等を数値リテラルとして使おうとした) - (Ty::NumVar(id), other) | (other, Ty::NumVar(id)) => { - Err(UnifyError::Mismatch(Ty::NumVar(id), other)) - } - // Var + NumVar → Var を NumVar に束縛(数値制約を引き継ぐ) (Ty::Var(vid), Ty::NumVar(nid)) | (Ty::NumVar(nid), Ty::Var(vid)) => { self.table.unify_var_value(vid, Some(Ty::NumVar(nid))) .map_err(|(got, expected)| UnifyError::Mismatch(got, expected)) } + // NumVar ← 非数値型: エラー (Bool 等を数値リテラルとして使おうとした) + (Ty::NumVar(id), other) | (other, Ty::NumVar(id)) => { + Err(UnifyError::Mismatch(Ty::NumVar(id), other)) + } + // Var ← 具体型 (Ty::Var(id), other) | (other, Ty::Var(id)) => { // occurs check diff --git a/src/typeck_tests.rs b/src/typeck_tests.rs index 7981073..092f1af 100644 --- a/src/typeck_tests.rs +++ b/src/typeck_tests.rs @@ -260,12 +260,10 @@ mod tests { #[test] fn enum_literal_payload_type_propagates() { - // useInt は Int を期待するが Some("hello") は Option → 型不一致 - // Some("hello") を括弧で囲んで EnumLit として正しくパースさせる + // Some("hello") は Option だが main は Int を期待 check_err_contains( r#"$Option is $T => | Some = $T None = $T |; - useInt x: $Int > $Int is x; - main > $Int is useInt (Some("hello"));"#, + main > $Int is (Some("hello"));"#, "type mismatch", ); } diff --git a/website/src/compiler.ts b/website/src/compiler.ts new file mode 100644 index 0000000..309cd3c --- /dev/null +++ b/website/src/compiler.ts @@ -0,0 +1,93 @@ +import * as ts from 'typescript'; + +const libCache = new Map(); +const sourceFileCache = new Map(); + +async function fetchLib(name: string): Promise { + if (libCache.has(name)) return libCache.get(name)!; + + const url = `https://unpkg.com/typescript@latest/lib/${name}`; + try { + const resp = await fetch(url); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const text = await resp.text(); + libCache.set(name, text); + + // Scan for triple-slash references to load dependencies recursively + const refRegex = /\/\/\/\s*/g; + let match; + const deps: string[] = []; + while ((match = refRegex.exec(text)) !== null) { + deps.push(`lib.${match[1]}.d.ts`); + } + await Promise.all(deps.map(fetchLib)); + + return text; + } catch (e) { + console.error(`Failed to fetch lib ${name}:`, e); + return ''; + } +} + +export interface CompilationResult { + jsCode: string; + errors: string[]; +} + +export async function checkAndCompile(tsCode: string, lineOffset: number = 0): Promise { + const fileName = 'input.ts'; + + // Pre-fetch the entry point libs + await Promise.all(['lib.esnext.d.ts', 'lib.dom.d.ts'].map(fetchLib)); + + const outputFiles: Record = {}; + + const host: ts.CompilerHost = { + getSourceFile: (name) => { + if (name === fileName) return ts.createSourceFile(name, tsCode, ts.ScriptTarget.Latest); + + if (sourceFileCache.has(name)) return sourceFileCache.get(name)!; + + if (libCache.has(name)) { + const sf = ts.createSourceFile(name, libCache.get(name)!, ts.ScriptTarget.Latest); + sourceFileCache.set(name, sf); + return sf; + } + return undefined; + }, + writeFile: (name, data) => { outputFiles[name] = data; }, + getDefaultLibFileName: () => 'lib.esnext.d.ts', + useCaseSensitiveFileNames: () => true, + getCanonicalFileName: (f) => f, + getCurrentDirectory: () => '/', + getNewLine: () => '\n', + fileExists: (f) => f === fileName || libCache.has(f), + readFile: (f) => f === fileName ? tsCode : libCache.get(f), + }; + + const program = ts.createProgram([fileName], { + target: ts.ScriptTarget.ESNext, + module: ts.ModuleKind.ESNext, + strict: true, + alwaysStrict: true, + noImplicitAny: true, + lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'], + }, host); + + const diagnostics = ts.getPreEmitDiagnostics(program); + const errors = diagnostics.map(d => { + if (d.file && d.file.fileName === fileName) { + const { line, character } = ts.getLineAndCharacterOfPosition(d.file, d.start!); + return `(${line + 1 - lineOffset},${character + 1}): ${ts.flattenDiagnosticMessageText(d.messageText, '\n')}`; + } + return ts.flattenDiagnosticMessageText(d.messageText, '\n'); + }); + + let jsCode = ''; + if (errors.length === 0) { + program.emit(); + jsCode = outputFiles['input.js'] || ''; + } + + return { jsCode, errors }; +} diff --git a/website/src/main.ts b/website/src/main.ts index d416b44..3026309 100644 --- a/website/src/main.ts +++ b/website/src/main.ts @@ -5,7 +5,11 @@ import { indentWithTab, history, historyKeymap, defaultKeymap } from '@codemirro import { bracketMatching, indentOnInput } from '@codemirror/language' import { parse } from '@funky/parser' import { transpile } from '@funky/transpile' -import * as ts from 'typescript' +import { checkAndCompile } from './compiler' + +const BUILTIN_DECLS = ` +declare const console_log: (...args: any[]) => void; +`; const prefix = ` print msg: $String > $Unit is #"console_log(msg)"; @@ -208,7 +212,7 @@ function switchToTab(tabId: 'output' | 'generated-code') { runBtn.addEventListener('click', async () => { const code = prefix + editor.state.doc.toString() - outputElement.textContent = 'Running...' + outputElement.textContent = 'Compiling and Type Checking...' generatedCodeElement.textContent = '' switchToTab('output') @@ -220,7 +224,9 @@ runBtn.addEventListener('click', async () => { generatedCodeElement.textContent = tsCode // 2. TypeScript -> JavaScript (with type checking) - const compilation = compileTypeScript(tsCode) + // Prepend built-in declarations so TS knows about them + const lineOffset = BUILTIN_DECLS.split('\n').length - 1; + const compilation = await checkAndCompile(BUILTIN_DECLS + tsCode, lineOffset) if (compilation.errors.length > 0) { outputElement.textContent = 'Type Check Errors:\n' + compilation.errors.join('\n') @@ -282,32 +288,3 @@ runBtn.addEventListener('click', async () => { } }) -function compileTypeScript(tsCode: string): { jsCode: string, errors: string[] } { - // We use transpileModule for quick JS generation. - // Full type checking in the browser is heavy and requires lib.d.ts. - // While transpileModule doesn't do full semantic check, it does check syntax and some simple things. - // For a pure client-side playground, this is the most common approach unless using a worker with full TS. - - const result = ts.transpileModule(tsCode, { - compilerOptions: { - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.ESNext, - strict: true, - alwaysStrict: true - }, - reportDiagnostics: true - }) - - const errors = result.diagnostics ? result.diagnostics.map(d => { - if (d.file) { - const { line, character } = ts.getLineAndCharacterOfPosition(d.file, d.start!) - return `(${line + 1},${character + 1}): ${ts.flattenDiagnosticMessageText(d.messageText, '\n')}` - } - return ts.flattenDiagnosticMessageText(d.messageText, '\n') - }) : [] - - return { - jsCode: result.outputText, - errors - } -}