Skip to content

Commit 8575fe1

Browse files
committed
Module system fix, refactoring, and tests
1 parent 578c1ad commit 8575fe1

11 files changed

Lines changed: 333 additions & 242 deletions

File tree

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ScriptML is a research project and toy compiler, written in TypeScript, that exp
1212
- **Recursion and totality checks**
1313
- **Dependent function types (Π-types)**
1414
- **IO via embedded thunks** that lower to JS functions
15+
- **Modules and imports** with qualified names and `open`
1516

1617
The compiler emits either standalone JavaScript or directly evaluates programs.
1718

@@ -46,7 +47,7 @@ npm test
4647

4748
## Language overview
4849

49-
ScriptML has a **typed λ-calculus core** with Nats, Lists, user ADTs, and IO.
50+
ScriptML has a **typed λ-calculus core** with Nats, Lists, user ADTs, IO, and now **modules**.
5051

5152
### Basic expressions
5253

@@ -116,6 +117,48 @@ in
116117
length [zero, succ zero, succ (succ zero)]
117118
```
118119
120+
---
121+
122+
### Modules
123+
124+
ScriptML supports a lightweight module system.
125+
126+
* A file can begin with `module <Name>` (optional; defaults to the filename).
127+
* Import another file with `import "path/to/file.sml" as Alias`.
128+
* Use qualified names like `Math.add`.
129+
* Use `open <Module>` to bring a module’s values into scope without qualification.
130+
131+
Example module:
132+
133+
```ocaml
134+
module Math
135+
136+
val add = fun (a: _) -> fun (b: _) -> a + b;
137+
val sub = fun (a: _) -> fun (b: _) -> a - b;
138+
```
139+
140+
Use it:
141+
142+
```ocaml
143+
import "std/math.sml" as Math
144+
145+
println ("3 + 4 = " ^ Math.add 3 4)
146+
```
147+
148+
Or with `open`:
149+
150+
```ocaml
151+
import "std/math.sml" as Math
152+
open Math
153+
154+
println ("3 - 4 = " ^ sub 3 4)
155+
```
156+
157+
All `val` and `type` decls in a module are exported by default.
158+
`open` is just syntactic sugar for creating local aliases.
159+
160+
---
161+
119162
### IO
120163
121164
IO is modeled with thunks (`() => value`) and `bind` / `pure`:
@@ -144,9 +187,14 @@ Builtins provided via `__io`:
144187
A simplified EBNF for ScriptML:
145188
146189
```
147-
program ::= { decl } term
190+
program ::= [ "module" Ident ] { import | decl } [ term ]
191+
192+
import ::= "import" Str "as" Ident
193+
| "open" Ident
148194
149195
decl ::= "type" Ident { Ident } "=" ctor { "|" ctor }
196+
| "val" Ident "=" term ";"
197+
150198
ctor ::= Ident [ "of" type { "," type } ]
151199
152200
term ::= let | fun | if | match | expr
@@ -163,6 +211,7 @@ pattern ::= "zero"
163211
164212
expr ::= expr atom | atom
165213
atom ::= Ident
214+
| Ident "." Ident // qualified variable
166215
| Int
167216
| Str
168217
| "zero"
@@ -172,6 +221,7 @@ atom ::= Ident
172221
173222
type ::= type "->" type
174223
| "Nat"
224+
| "Unit"
175225
| Ident
176226
| Ident type
177227
| "(" type ")"
@@ -244,11 +294,11 @@ play 21
244294
* `lexer.ts`
245295
* `parser.ts` (Pratt parser)
246296
* `elab_emit.ts` (elaboration + JS codegen)
247-
* `compiler.ts` (top-level driver)
297+
* `compiler.ts` (top-level driver, import/module loader)
248298
* Tests in `__tests__`, goldens in `tests/fixtures`
249299
250300
---
251301
252302
## License
253303
254-
MIT
304+
MIT

__tests__/golden.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, test, expect} from "@jest/globals";
2-
import {compileToStandaloneJS, runJSString, fixture, normalizeOut} from "./helpers";
2+
import { compileToStandaloneJS, runJSString, fixture, normalizeOut, compileFileToStandaloneJS } from "./helpers";
33

44
function golden(name: string, answersFile?: string) {
55
const src = fixture(`${name}.sml`);
@@ -14,6 +14,35 @@ function golden(name: string, answersFile?: string) {
1414
expect(actual).toBe(expected);
1515
}
1616

17+
function goldenEntry(entryRel: string, stdoutFixtureRel: string, stdin?: string) {
18+
const js = compileFileToStandaloneJS(entryRel);
19+
const res = runJSString(js, stdin);
20+
if (res.status !== 0) {
21+
throw new Error(
22+
`node exited with ${res.status}\nSTDERR:\n${res.stderr}\nSTDOUT:\n${res.stdout}`
23+
);
24+
}
25+
const expected = normalizeOut(fixture(stdoutFixtureRel)).trim();
26+
const actual = normalizeOut(res.stdout).trim();
27+
expect(actual).toBe(expected);
28+
}
29+
30+
describe("Modules", () => {
31+
test("qualified import: use_math_qualified.sml", () => {
32+
goldenEntry(
33+
"tests/fixtures/use_math_qualified.sml",
34+
"use_math_qualified.stdout"
35+
);
36+
});
37+
38+
test("open import: use_math_open.sml", () => {
39+
goldenEntry(
40+
"tests/fixtures/use_math_open.sml",
41+
"use_math_open.stdout"
42+
);
43+
});
44+
});
45+
1746
describe("Golden programs", () => {
1847
test('nim_21.sml', () => {
1948
golden('nim_21', 'nim_21.answers');

__tests__/helpers.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import * as fs from "fs";
22
import * as path from "path";
3-
import {spawnSync} from "child_process";
4-
import {compile} from "../src/compiler";
3+
import { spawnSync } from "child_process";
4+
import { compile, compileFile } from "../src/compiler";
55

6-
/** Compile SML to a standalone JS string (with embedded __io runtime). */
6+
/** Compile SML source text to a standalone JS string (with embedded __io runtime). */
77
export function compileToStandaloneJS(src: string): string {
88
return String(compile(src, "js"));
99
}
1010

11+
/** Compile an ENTRY FILE (resolving imports/open) to a standalone JS string. */
12+
export function compileFileToStandaloneJS(entryRelOrAbs: string): string {
13+
const entry = path.isAbsolute(entryRelOrAbs)
14+
? entryRelOrAbs
15+
: path.resolve(process.cwd(), entryRelOrAbs);
16+
return String(compileFile(entry, "js"));
17+
}
18+
1119
const ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g; // robust ANSI escape matcher
1220
export const normalizeOut = (s: string) =>
1321
s.replace(ANSI_RE, "").replace(/\r\n/g, "\n");
1422

1523
/** Run a standalone JS string with Node, capturing stdout/stderr as strings. */
1624
export function runJSString(js: string, stdin?: string) {
1725
const tmp = path.join(process.cwd(), "tests", ".tmp");
18-
fs.mkdirSync(tmp, {recursive: true});
26+
fs.mkdirSync(tmp, { recursive: true });
1927
const file = path.join(
2028
tmp,
2129
`prog_${Date.now()}_${Math.random().toString(36).slice(2)}.js`

0 commit comments

Comments
 (0)