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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/dist
/dist
/target
/node_modules
.DS_Store
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions src/typeck_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,10 @@ mod tests {

#[test]
fn enum_literal_payload_type_propagates() {
// useInt は Int を期待するが Some("hello") は Option<String> → 型不一致
// Some("hello") を括弧で囲んで EnumLit として正しくパースさせる
// Some("hello") は Option<String> だが 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",
);
}
Expand Down
93 changes: 93 additions & 0 deletions website/src/compiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as ts from 'typescript';

const libCache = new Map<string, string>();
const sourceFileCache = new Map<string, ts.SourceFile>();

async function fetchLib(name: string): Promise<string> {
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*<reference\s+lib="([^"]+)"\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<CompilationResult> {
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<string, string> = {};

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 };
}
41 changes: 9 additions & 32 deletions website/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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
}
}
Loading