Skip to content

Commit 26ce8d6

Browse files
authored
Merge pull request #13 from OxfordAbstracts/codegen-passing
Codegen passing
2 parents 1cbd9a3 + 04c6b19 commit 26ce8d6

765 files changed

Lines changed: 70543 additions & 2653 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,16 @@ Thumbs.db
2828
/editors/code/out
2929
/editors/code/.vscode-test
3030
/editors/code/package-lock.json
31+
/editors/code/pfc-lsp-*
32+
33+
# purs
34+
/output
35+
36+
# test artifacts
37+
tests/fixtures/original-compiler/passing/*.output.js
38+
tests/fixtures/original-compiler/passing/*/*.output.js
39+
tests/fixtures/original-compiler/passing/*.error.txt
40+
tests/fixtures/original-compiler/passing/*/*.error.txt
41+
42+
# claude
43+
.claude/worktrees

Cargo.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ miette = { version = "7.6", features = ["fancy"] }
2222
thiserror = "2.0"
2323
lalrpop-util = "0.22"
2424
glob = "0.3"
25-
swc_ecma_parser = "34.0.0"
26-
swc_ecma_ast = "20.0.1"
27-
swc_common = "18.0.1"
25+
swc_ecma_parser = "35.0.0"
26+
swc_ecma_ast = "21.0.0"
27+
swc_common = "19.0.0"
28+
swc_ecma_codegen = "24.0.0"
2829
ntest_timeout = "0.9.5"
2930
rayon = "1.10"
31+
stacker = "0.1"
3032
mimalloc = { version = "0.1", default-features = false }
3133
tower-lsp = "0.20"
3234
tokio = { version = "1", features = ["full"] }
@@ -43,3 +45,5 @@ insta = "1.34"
4345
criterion = "0.5"
4446
proptest = "1.4"
4547
regex = "1"
48+
tempfile = "3.27.0"
49+
wait-timeout = "0.2.1"

editors/code/package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{
22
"name": "pfc-lsp",
3-
"displayName": "PureScript Fast Compiler",
3+
"displayName": "PureScript Fast Compiler LSP Client",
44
"description": "VS Code client for the pfc language server",
55
"version": "0.0.1",
6-
"publisher": "pfc",
6+
"publisher": "OxfordAbstracts",
7+
"repository": "https://github.com/OxfordAbstracts/purescript-fast-compiler",
78
"engines": {
89
"vscode": "^1.75.0"
910
},
@@ -49,6 +50,16 @@
4950
"type": "string",
5051
"default": "ragu sources",
5152
"description": "Shell command that outputs PureScript source file paths (one per line). Example: find src .spago/p -name '*.purs'"
53+
},
54+
"pfc.outputDir": {
55+
"type": "string",
56+
"default": "",
57+
"description": "Output directory for generated JavaScript. When set, the LSP will generate JS code on save. Example: output"
58+
},
59+
"pfc.outputDirCommand": {
60+
"type": "string",
61+
"default": "ragu output-dir",
62+
"description": "Shell command that outputs the JS output directory path. When set, overrides pfc.outputDir. Example: spago path output"
5263
}
5364
}
5465
}
@@ -61,6 +72,7 @@
6172
"vscode-languageclient": "^9.0.1"
6273
},
6374
"devDependencies": {
75+
"@types/node": "^20.0.0",
6476
"@types/vscode": "^1.75.0",
6577
"typescript": "^5.0.0"
6678
}

editors/code/src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ export function activate(context: vscode.ExtensionContext) {
1111
const config = vscode.workspace.getConfiguration("pfc");
1212
const serverPath = config.get<string>("serverPath", "pfc");
1313
const sourcesCommand = config.get<string>("sourcesCommand", "");
14+
const outputDir = config.get<string>("outputDir", "");
1415

1516
const args = ["lsp"];
1617
if (sourcesCommand) {
1718
args.push("--sources-cmd", sourcesCommand);
1819
}
20+
if (outputDir) {
21+
args.push("--output-dir", outputDir);
22+
}
1923

2024
const serverOptions: ServerOptions = {
2125
command: serverPath,

src/ast.rs

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ pub enum Expr {
291291
/// Typed hole: ?hole
292292
Hole { span: Span, name: Ident },
293293

294+
/// Wildcard: _ (anonymous argument, NOT a typed hole)
295+
Wildcard { span: Span },
296+
294297
/// Array literal: [1, 2, 3]
295298
Array { span: Span, elements: Vec<Expr> },
296299

@@ -580,6 +583,7 @@ impl Expr {
580583
| Expr::RecordUpdate { span, .. }
581584
| Expr::TypeAnnotation { span, .. }
582585
| Expr::Hole { span, .. }
586+
| Expr::Wildcard { span, .. }
583587
| Expr::Array { span, .. }
584588
| Expr::Negate { span, .. }
585589
| Expr::AsPattern { span, .. }
@@ -1697,6 +1701,10 @@ impl Converter {
16971701
// --- Expression conversion ---
16981702

16991703
fn convert_expr(&mut self, expr: &cst::Expr) -> Expr {
1704+
stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, || self.convert_expr_impl(expr))
1705+
}
1706+
1707+
fn convert_expr_impl(&mut self, expr: &cst::Expr) -> Expr {
17001708
match expr {
17011709
cst::Expr::Var { span, name } => Expr::Var {
17021710
span: *span,
@@ -1931,10 +1939,87 @@ impl Converter {
19311939
.map(|f| self.convert_record_field(f))
19321940
.collect(),
19331941
},
1934-
cst::Expr::RecordAccess { span, expr, field } => Expr::RecordAccess {
1935-
span: *span,
1936-
expr: Box::new(self.convert_expr(expr)),
1937-
field: field.clone(),
1942+
cst::Expr::RecordAccess { span, expr, field } => {
1943+
// _.x (record accessor section) → \$_arg -> $_arg.x
1944+
if Self::is_wildcard(expr) {
1945+
let param_name = intern("$_arg");
1946+
let mut scope = HashMap::new();
1947+
scope.insert(param_name, *span);
1948+
self.local_scopes.push(scope);
1949+
let param_expr = Expr::Var {
1950+
span: expr.span(),
1951+
name: QualifiedIdent {
1952+
module: None,
1953+
name: param_name,
1954+
},
1955+
definition_site: DefinitionSite::Local(*span),
1956+
};
1957+
let body = Expr::RecordAccess {
1958+
span: *span,
1959+
expr: Box::new(param_expr),
1960+
field: field.clone(),
1961+
};
1962+
self.local_scopes.pop();
1963+
Expr::Lambda {
1964+
span: *span,
1965+
binders: vec![Binder::Var {
1966+
span: *span,
1967+
name: cst::Spanned {
1968+
span: *span,
1969+
value: param_name,
1970+
},
1971+
}],
1972+
body: Box::new(body),
1973+
}
1974+
} else {
1975+
// Handle chained accessor on wildcard: _.x.y → \$_arg -> $_arg.x.y
1976+
let mut chain = vec![field.clone()];
1977+
let mut inner = expr.as_ref();
1978+
while let cst::Expr::RecordAccess { expr: e, field: f, .. } = inner {
1979+
chain.push(f.clone());
1980+
inner = e.as_ref();
1981+
}
1982+
if Self::is_wildcard(inner) {
1983+
let param_name = intern("$_arg");
1984+
let mut scope = HashMap::new();
1985+
scope.insert(param_name, *span);
1986+
self.local_scopes.push(scope);
1987+
let mut body = Expr::Var {
1988+
span: inner.span(),
1989+
name: QualifiedIdent {
1990+
module: None,
1991+
name: param_name,
1992+
},
1993+
definition_site: DefinitionSite::Local(*span),
1994+
};
1995+
// Apply fields in reverse (innermost first): _.x.y → ($__arg).x.y
1996+
for f in chain.iter().rev() {
1997+
body = Expr::RecordAccess {
1998+
span: *span,
1999+
expr: Box::new(body),
2000+
field: f.clone(),
2001+
};
2002+
}
2003+
self.local_scopes.pop();
2004+
Expr::Lambda {
2005+
span: *span,
2006+
binders: vec![Binder::Var {
2007+
span: *span,
2008+
name: cst::Spanned {
2009+
span: *span,
2010+
value: param_name,
2011+
},
2012+
}],
2013+
body: Box::new(body),
2014+
}
2015+
} else {
2016+
Expr::RecordAccess {
2017+
span: *span,
2018+
expr: Box::new(self.convert_expr(expr)),
2019+
field: field.clone(),
2020+
}
2021+
}
2022+
}
19382023
},
19392024
cst::Expr::RecordUpdate {
19402025
span,
@@ -1975,9 +2060,8 @@ impl Converter {
19752060
span: *span,
19762061
name: *name,
19772062
},
1978-
cst::Expr::Wildcard { span } => Expr::Hole {
2063+
cst::Expr::Wildcard { span } => Expr::Wildcard {
19792064
span: *span,
1980-
name: intern("_"),
19812065
},
19822066
cst::Expr::Array { span, elements } => Expr::Array {
19832067
span: *span,
@@ -2204,8 +2288,7 @@ impl Converter {
22042288
return result;
22052289
}
22062290

2207-
let wildcard_sym = interner::intern("_");
2208-
// Destructure App(App(op, left), right) to check for holes
2291+
// Destructure App(App(op, left), right) to check for wildcard sections
22092292
if let Expr::App {
22102293
span: outer_span,
22112294
func: outer_func,
@@ -2218,10 +2301,8 @@ impl Converter {
22182301
arg: left_arg,
22192302
} = *outer_func
22202303
{
2221-
let left_is_hole =
2222-
matches!(&*left_arg, Expr::Hole { name, .. } if *name == wildcard_sym);
2223-
let right_is_hole =
2224-
matches!(&*right_arg, Expr::Hole { name, .. } if *name == wildcard_sym);
2304+
let left_is_hole = matches!(&*left_arg, Expr::Wildcard { .. });
2305+
let right_is_hole = matches!(&*right_arg, Expr::Wildcard { .. });
22252306
if left_is_hole || right_is_hole {
22262307
// Valid section after rebalancing — desugar to lambda
22272308
let param_name = interner::intern("$_arg");
@@ -2278,10 +2359,7 @@ impl Converter {
22782359
// but emit error for safety
22792360
self.errors
22802361
.push(TypeError::IncorrectAnonymousArgument { span });
2281-
output.pop().unwrap_or(Expr::Hole {
2282-
span,
2283-
name: wildcard_sym,
2284-
})
2362+
output.pop().unwrap_or(Expr::Wildcard { span })
22852363
}
22862364

22872365
fn build_op_app(
@@ -2363,6 +2441,10 @@ impl Converter {
23632441
// --- Type expression conversion ---
23642442

23652443
fn convert_type_expr(&mut self, ty: &cst::TypeExpr) -> TypeExpr {
2444+
stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, || self.convert_type_expr_impl(ty))
2445+
}
2446+
2447+
fn convert_type_expr_impl(&mut self, ty: &cst::TypeExpr) -> TypeExpr {
23662448
match ty {
23672449
cst::TypeExpr::Var { span, name } => TypeExpr::Var {
23682450
span: *span,
@@ -2610,6 +2692,10 @@ impl Converter {
26102692
// --- Binder conversion ---
26112693

26122694
fn convert_binder(&mut self, binder: &cst::Binder) -> Binder {
2695+
stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, || self.convert_binder_impl(binder))
2696+
}
2697+
2698+
fn convert_binder_impl(&mut self, binder: &cst::Binder) -> Binder {
26132699
match binder {
26142700
cst::Binder::Wildcard { span } => Binder::Wildcard { span: *span },
26152701
cst::Binder::Var { span, name } => Binder::Var {

src/build/cache.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,11 @@ impl ModuleCache {
663663
self.entries.get(module_name).map(|c| c.imports())
664664
}
665665

666+
/// Get the cache directory path, if configured.
667+
pub fn cache_dir(&self) -> Option<&Path> {
668+
self.cache_dir.as_deref()
669+
}
670+
666671
/// Get cached exports for a module, loading from disk if needed.
667672
pub fn get_exports(&mut self, module_name: &str) -> Option<&ModuleExports> {
668673
// Check if we need to load from disk first
@@ -863,7 +868,7 @@ impl ModuleCache {
863868

864869
// ===== File helpers =====
865870

866-
fn module_file_path(cache_dir: &Path, module_name: &str) -> PathBuf {
871+
pub fn module_file_path(cache_dir: &Path, module_name: &str) -> PathBuf {
867872
cache_dir.join("modules").join(format!("{}.bin", module_name))
868873
}
869874

@@ -884,7 +889,7 @@ fn save_module_file(path: &Path, exports: &ModuleExports) -> io::Result<()> {
884889
Ok(())
885890
}
886891

887-
fn load_module_file(path: &Path) -> io::Result<ModuleExports> {
892+
pub fn load_module_file(path: &Path) -> io::Result<ModuleExports> {
888893
let file = std::fs::File::open(path)?;
889894
let decoder = io::BufReader::new(zstd::Decoder::new(file)
890895
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("zstd: {e}")))?);

src/build/error.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ pub enum BuildError {
7171
path: PathBuf,
7272
message: String,
7373
},
74-
#[error("Typechecking timed out for module '{module_name}' at '{path}' (exceeded {timeout_secs}s)")]
75-
TypecheckTimeout {
76-
path: PathBuf,
77-
module_name: String,
78-
timeout_secs: u64,
79-
},
8074
}
8175

8276
impl BuildError {
@@ -97,7 +91,6 @@ impl BuildError {
9791
BuildError::UnsupportedFFICommonJSImports { .. } => "UnsupportedFFICommonJSImports".into(),
9892
BuildError::DeprecatedFFICommonJSModule { .. } => "DeprecatedFFICommonJSModule".into(),
9993
BuildError::FFIParseError { .. } => "FFIParseError".into(),
100-
BuildError::TypecheckTimeout { .. } => "TypecheckTimeout".into(),
10194
}
10295
}
10396
}

0 commit comments

Comments
 (0)