@sheldev/oryn 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, dirname, relative, extname, basename } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { parse } from "./parser.js";
6
+ import { transpile } from "./codegen.js";
7
+ export function compileSource(source, options) {
8
+ const ast = parse(source);
9
+ return transpile(ast, options);
10
+ }
11
+ export async function transpileFile(inputPath, outputPath) {
12
+ const source = await readFile(inputPath, "utf8");
13
+ const odinCode = compileSource(source);
14
+ if (outputPath) {
15
+ await writeFile(outputPath, odinCode, "utf8");
16
+ }
17
+ return odinCode;
18
+ }
19
+ /**
20
+ * Recursively transpile every `.ory` file under `inputDir` into `outputDir`,
21
+ * preserving the relative directory layout and swapping the `.ory` extension
22
+ * for `.odin`.
23
+ */
24
+ export async function transpileDirectory(inputDir, outputDir) {
25
+ const files = await collectOryFiles(inputDir);
26
+ for (const file of files) {
27
+ const rel = relative(inputDir, file);
28
+ const outFile = join(outputDir, rel.replace(/\.ory$/, ".odin"));
29
+ await mkdir(dirname(outFile), { recursive: true });
30
+ const source = await readFile(file, "utf8");
31
+ await writeFile(outFile, compileSource(source), "utf8");
32
+ }
33
+ return { count: files.length };
34
+ }
35
+ async function collectOryFiles(dir) {
36
+ const result = [];
37
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
38
+ const full = join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
41
+ continue;
42
+ result.push(...(await collectOryFiles(full)));
43
+ }
44
+ else if (extname(entry.name) === ".ory") {
45
+ result.push(full);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ /**
51
+ * Transpile `inputPath` and hand the generated Odin source to the native
52
+ * `odin` compiler. `-file` tells Odin to treat a single file as a
53
+ * self-contained package, which is exactly what we want for a single Oryn
54
+ * source file.
55
+ */
56
+ export async function buildWithOdin(inputPath, options = {}) {
57
+ const source = await readFile(inputPath, "utf8");
58
+ const odinCode = compileSource(source);
59
+ // Odin requires a writable directory for intermediate artifacts; use a
60
+ // temp dir so we never clobber anything the user cares about.
61
+ const { mkdtemp, rm } = await import("node:fs/promises");
62
+ const tempDir = await mkdtemp(join(tmpdir(), "oryn-build-"));
63
+ const base = basename(inputPath, extname(inputPath));
64
+ const odinFile = join(tempDir, `${base}.odin`);
65
+ try {
66
+ await writeFile(odinFile, odinCode, "utf8");
67
+ const subcommand = options.run ? "run" : "build";
68
+ const args = [subcommand, odinFile, "-file"];
69
+ if (!options.run) {
70
+ const outPath = options.outputBinary ?? base;
71
+ args.push(`-out:${outPath}`);
72
+ }
73
+ const result = spawnSync("odin", args, { stdio: "inherit" });
74
+ if (result.error) {
75
+ if (result.error.code === "ENOENT") {
76
+ throw new Error("Native 'odin' compiler not found in PATH. " +
77
+ "Install Odin to compile binaries; the transpiled Odin source is valid.");
78
+ }
79
+ throw result.error;
80
+ }
81
+ if (result.status !== 0) {
82
+ process.exitCode = result.status ?? 1;
83
+ }
84
+ }
85
+ finally {
86
+ await rm(tempDir, { recursive: true, force: true });
87
+ }
88
+ }
89
+ //# sourceMappingURL=compiler.js.map
@@ -0,0 +1,6 @@
1
+ export { Lexer, tokenize } from "./lexer.js";
2
+ export { Parser, parse, ParserError } from "./parser.js";
3
+ export { CodeGenerator, transpile, type CodegenOptions } from "./codegen.js";
4
+ export { compileSource, transpileFile, buildWithOdin } from "./compiler.js";
5
+ export * as AST from "./ast.js";
6
+ export { LexerError, type SourcePosition, type Token, type TokenKind } from "./token.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { Lexer, tokenize } from "./lexer.js";
2
+ export { Parser, parse, ParserError } from "./parser.js";
3
+ export { CodeGenerator, transpile } from "./codegen.js";
4
+ export { compileSource, transpileFile, buildWithOdin } from "./compiler.js";
5
+ export * as AST from "./ast.js";
6
+ export { LexerError } from "./token.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,28 @@
1
+ import { type Token } from "./token.js";
2
+ export declare class Lexer {
3
+ private readonly source;
4
+ private offset;
5
+ private line;
6
+ private column;
7
+ private readonly tokens;
8
+ constructor(source: string);
9
+ tokenize(): Token[];
10
+ private atEnd;
11
+ private peek;
12
+ private peekNext;
13
+ private advance;
14
+ private position;
15
+ private emit;
16
+ private single;
17
+ private emitNewline;
18
+ private lineComment;
19
+ private blockComment;
20
+ private string;
21
+ private number;
22
+ private identifier;
23
+ private operator;
24
+ private isDigit;
25
+ private isIdentifierStart;
26
+ private isIdentifierContinue;
27
+ }
28
+ export declare function tokenize(source: string): Token[];
package/dist/lexer.js ADDED
@@ -0,0 +1,305 @@
1
+ import { LexerError } from "./token.js";
2
+ const KEYWORDS = new Set([
3
+ "import",
4
+ "proc",
5
+ "var",
6
+ "let",
7
+ "const",
8
+ "struct",
9
+ "enum",
10
+ "type",
11
+ "if",
12
+ "else",
13
+ "for",
14
+ "while",
15
+ "return",
16
+ "break",
17
+ "continue",
18
+ "defer",
19
+ ]);
20
+ const OPERATORS = [
21
+ "...", "..=", "->", "=>", ":=", "==", "!=", "<=", ">=",
22
+ "&&", "||", "+=", "-=", "*=", "/=", "%=", "&=", "|=",
23
+ "<<", ">>", "..", "and", "or", "not",
24
+ ];
25
+ const SINGLE_OPERATORS = new Set("+-*/%=<>!&|^~?=".split(""));
26
+ const PUNCTUATION = new Set("(){}[],:;.".split(""));
27
+ export class Lexer {
28
+ source;
29
+ offset = 0;
30
+ line = 1;
31
+ column = 1;
32
+ tokens = [];
33
+ constructor(source) {
34
+ this.source = source;
35
+ }
36
+ tokenize() {
37
+ while (!this.atEnd()) {
38
+ const char = this.peek();
39
+ if (char === " " || char === "\t" || char === "\r") {
40
+ this.advance();
41
+ continue;
42
+ }
43
+ if (char === "\n") {
44
+ this.emitNewline();
45
+ continue;
46
+ }
47
+ if (char === "/" && this.peekNext() === "/") {
48
+ this.lineComment();
49
+ continue;
50
+ }
51
+ if (char === "/" && this.peekNext() === "*") {
52
+ this.blockComment();
53
+ continue;
54
+ }
55
+ if (char === '"' || char === "'") {
56
+ this.string();
57
+ continue;
58
+ }
59
+ if (this.isDigit(char)) {
60
+ this.number();
61
+ continue;
62
+ }
63
+ if (this.isIdentifierStart(char)) {
64
+ this.identifier();
65
+ continue;
66
+ }
67
+ if (this.operator()) {
68
+ continue;
69
+ }
70
+ if (PUNCTUATION.has(char)) {
71
+ this.single("punctuation");
72
+ continue;
73
+ }
74
+ if (SINGLE_OPERATORS.has(char)) {
75
+ this.single("operator");
76
+ continue;
77
+ }
78
+ throw new LexerError(`Unexpected character ${JSON.stringify(char)}`, this.position());
79
+ }
80
+ const position = this.position();
81
+ this.tokens.push({
82
+ kind: "eof",
83
+ value: "",
84
+ start: position,
85
+ end: position,
86
+ });
87
+ return this.tokens;
88
+ }
89
+ atEnd() {
90
+ return this.offset >= this.source.length;
91
+ }
92
+ peek(distance = 0) {
93
+ return this.source[this.offset + distance] ?? "";
94
+ }
95
+ peekNext() {
96
+ return this.peek(1);
97
+ }
98
+ advance() {
99
+ const char = this.source[this.offset++] ?? "";
100
+ if (char === "\n") {
101
+ this.line++;
102
+ this.column = 1;
103
+ }
104
+ else {
105
+ this.column++;
106
+ }
107
+ return char;
108
+ }
109
+ position() {
110
+ return { offset: this.offset, line: this.line, column: this.column };
111
+ }
112
+ emit(kind, value, start) {
113
+ this.tokens.push({ kind, value, start, end: this.position() });
114
+ }
115
+ single(kind) {
116
+ const start = this.position();
117
+ const value = this.advance();
118
+ this.emit(kind, value, start);
119
+ }
120
+ emitNewline() {
121
+ const start = this.position();
122
+ this.advance();
123
+ this.emit("newline", "\n", start);
124
+ }
125
+ lineComment() {
126
+ while (!this.atEnd() && this.peek() !== "\n")
127
+ this.advance();
128
+ }
129
+ blockComment() {
130
+ const start = this.position();
131
+ this.advance();
132
+ this.advance();
133
+ while (!this.atEnd()) {
134
+ if (this.peek() === "*" && this.peekNext() === "/") {
135
+ this.advance();
136
+ this.advance();
137
+ return;
138
+ }
139
+ this.advance();
140
+ }
141
+ throw new LexerError("Unterminated block comment", start);
142
+ }
143
+ string() {
144
+ const start = this.position();
145
+ const quote = this.advance();
146
+ let value = quote;
147
+ while (!this.atEnd()) {
148
+ const char = this.advance();
149
+ value += char;
150
+ if (char === "\\") {
151
+ if (!this.atEnd())
152
+ value += this.advance();
153
+ continue;
154
+ }
155
+ if (char === quote) {
156
+ this.emit("string", value, start);
157
+ return;
158
+ }
159
+ if (char === "\n") {
160
+ throw new LexerError("Unterminated string literal", start);
161
+ }
162
+ }
163
+ throw new LexerError("Unterminated string literal", start);
164
+ }
165
+ number() {
166
+ const start = this.position();
167
+ let value = "";
168
+ if (this.peek() === "0" && /[xXbBoO]/.test(this.peekNext())) {
169
+ value += this.advance();
170
+ const prefix = this.advance().toLowerCase();
171
+ value += prefix;
172
+ const isValidDigit = (c) => {
173
+ if (prefix === "x")
174
+ return /[0-9a-fA-F]/.test(c);
175
+ if (prefix === "b")
176
+ return /[01]/.test(c);
177
+ if (prefix === "o")
178
+ return /[0-7]/.test(c);
179
+ return false;
180
+ };
181
+ if (!isValidDigit(this.peek())) {
182
+ throw new LexerError(`Malformed ${prefix === "x" ? "hexadecimal" : prefix === "b" ? "binary" : "octal"} literal`, start);
183
+ }
184
+ let lastWasUnderscore = false;
185
+ while (!this.atEnd()) {
186
+ const c = this.peek();
187
+ if (isValidDigit(c)) {
188
+ value += this.advance();
189
+ lastWasUnderscore = false;
190
+ }
191
+ else if (c === "_") {
192
+ if (lastWasUnderscore) {
193
+ throw new LexerError("Malformed numeric literal: consecutive underscores", start);
194
+ }
195
+ value += this.advance();
196
+ lastWasUnderscore = true;
197
+ }
198
+ else if (this.isIdentifierContinue(c) || this.isDigit(c)) {
199
+ throw new LexerError(`Malformed ${prefix === "x" ? "hexadecimal" : prefix === "b" ? "binary" : "octal"} literal`, start);
200
+ }
201
+ else {
202
+ break;
203
+ }
204
+ }
205
+ if (lastWasUnderscore) {
206
+ throw new LexerError("Malformed numeric literal: trailing underscore", start);
207
+ }
208
+ this.emit("number", value, start);
209
+ return;
210
+ }
211
+ let lastWasUnderscore = false;
212
+ let hasDecimalPoint = false;
213
+ while (!this.atEnd()) {
214
+ const c = this.peek();
215
+ if (this.isDigit(c)) {
216
+ value += this.advance();
217
+ lastWasUnderscore = false;
218
+ }
219
+ else if (c === "_") {
220
+ if (lastWasUnderscore) {
221
+ throw new LexerError("Malformed numeric literal: consecutive underscores", start);
222
+ }
223
+ value += this.advance();
224
+ lastWasUnderscore = true;
225
+ }
226
+ else if (c === ".") {
227
+ if (this.peek(1) === ".") {
228
+ break;
229
+ }
230
+ if (hasDecimalPoint) {
231
+ break;
232
+ }
233
+ if (!this.isDigit(this.peek(1))) {
234
+ throw new LexerError("Malformed numeric literal: trailing decimal point", start);
235
+ }
236
+ hasDecimalPoint = true;
237
+ value += this.advance();
238
+ lastWasUnderscore = false;
239
+ }
240
+ else if (c.toLowerCase() === "e") {
241
+ value += this.advance();
242
+ if (this.peek() === "+" || this.peek() === "-") {
243
+ value += this.advance();
244
+ }
245
+ if (!this.isDigit(this.peek())) {
246
+ throw new LexerError("Malformed numeric exponent", start);
247
+ }
248
+ while (this.isDigit(this.peek()) || this.peek() === "_") {
249
+ value += this.advance();
250
+ }
251
+ break;
252
+ }
253
+ else if (this.isIdentifierStart(c) || (this.isIdentifierContinue(c) && !this.isDigit(c))) {
254
+ throw new LexerError("Malformed numeric literal", start);
255
+ }
256
+ else {
257
+ break;
258
+ }
259
+ }
260
+ if (lastWasUnderscore) {
261
+ throw new LexerError("Malformed numeric literal: trailing underscore", start);
262
+ }
263
+ this.emit("number", value, start);
264
+ }
265
+ identifier() {
266
+ const start = this.position();
267
+ let value = this.advance();
268
+ while (this.isIdentifierContinue(this.peek())) {
269
+ value += this.advance();
270
+ }
271
+ const kind = KEYWORDS.has(value) ? "keyword" : "identifier";
272
+ this.emit(kind, value, start);
273
+ }
274
+ operator() {
275
+ for (const operator of OPERATORS) {
276
+ if (this.source.startsWith(operator, this.offset)) {
277
+ if (/^[a-zA-Z_]+$/.test(operator)) {
278
+ const nextChar = this.source[this.offset + operator.length] ?? "";
279
+ if (/[a-zA-Z0-9_]/.test(nextChar)) {
280
+ continue;
281
+ }
282
+ }
283
+ const start = this.position();
284
+ for (let i = 0; i < operator.length; i++)
285
+ this.advance();
286
+ this.emit("operator", operator, start);
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+ isDigit(char) {
293
+ return char >= "0" && char <= "9";
294
+ }
295
+ isIdentifierStart(char) {
296
+ return /[A-Za-z_]/.test(char);
297
+ }
298
+ isIdentifierContinue(char) {
299
+ return /[A-Za-z0-9_]/.test(char);
300
+ }
301
+ }
302
+ export function tokenize(source) {
303
+ return new Lexer(source).tokenize();
304
+ }
305
+ //# sourceMappingURL=lexer.js.map
@@ -0,0 +1,52 @@
1
+ import { type Token, type SourcePosition } from "./token.js";
2
+ import * as AST from "./ast.js";
3
+ export declare class ParserError extends Error {
4
+ readonly position: SourcePosition;
5
+ constructor(message: string, position: SourcePosition);
6
+ }
7
+ export declare class Parser {
8
+ private readonly tokens;
9
+ private current;
10
+ constructor(tokens: Token[]);
11
+ parse(): AST.Program;
12
+ private parseImport;
13
+ private parseStruct;
14
+ private parseEnum;
15
+ private parseTypeAnnotation;
16
+ private parseProc;
17
+ private parseVarDecl;
18
+ private parseBlock;
19
+ private parseStatement;
20
+ private parseReturn;
21
+ private parseIf;
22
+ private parseWhile;
23
+ private parseFor;
24
+ private parseExpression;
25
+ private parseOrHandling;
26
+ private parseLambda;
27
+ private parseLogicalOr;
28
+ private parseLogicalAnd;
29
+ private parseEquality;
30
+ private parseComparison;
31
+ private parseTerm;
32
+ private parseFactor;
33
+ private parseUnary;
34
+ private parseCallMember;
35
+ private finishCall;
36
+ private parsePrimary;
37
+ private peek;
38
+ private previous;
39
+ private isAtEnd;
40
+ private advance;
41
+ private match;
42
+ private matchKeyword;
43
+ private matchPunctuation;
44
+ private checkPunctuation;
45
+ private matchOperator;
46
+ private consume;
47
+ private consumePunctuation;
48
+ private isAssignmentOperator;
49
+ private isSeparator;
50
+ private skipSeparators;
51
+ }
52
+ export declare function parse(source: string): AST.Program;