nomen-lang 0.0.1

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.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "nomen-lang",
3
+ "version": "0.0.1",
4
+ "description": "The CLI for the Nomen programming language.",
5
+ "keywords": [],
6
+ "license": "ISC",
7
+ "author": "",
8
+ "bin": {
9
+ "nomen": "dist/index.mjs"
10
+ },
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "vp pack",
14
+ "dev": "vp pack --watch",
15
+ "test": "vp test",
16
+ "check": "vp check",
17
+ "format": "vp check --fix",
18
+ "prepare": "vp config",
19
+ "go": "tsx src/index.ts",
20
+ "register": "pnpm build && pnpm add -g ."
21
+ },
22
+ "dependencies": {
23
+ "chokidar": "^5.0.0",
24
+ "yargs": "^18.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^26.1.1",
28
+ "@types/yargs": "^17.0.35",
29
+ "tsx": "^4.23.1",
30
+ "typescript": "^7.0.2",
31
+ "vite-plus": "catalog:",
32
+ "vitest": "catalog:"
33
+ }
34
+ }
@@ -0,0 +1,86 @@
1
+ import path from "node:path";
2
+
3
+ import type CompileError from "../../src/types/CompileError.ts";
4
+
5
+ interface FileMarker {
6
+ abs_path: string;
7
+ joined_line: number;
8
+ }
9
+
10
+ // Scan the joined source for `// file://<abs path>` headers inserted by join(),
11
+ // recording the 1-based joined-source line number each header sits on.
12
+ function find_file_markers(source: string): FileMarker[] {
13
+ const markers: FileMarker[] = [];
14
+ let line = 1;
15
+ let i = 0;
16
+ while (i < source.length) {
17
+ const nl = source.indexOf("\n", i);
18
+ const end = nl === -1 ? source.length : nl;
19
+ const match = source.slice(i, end).match(/^\/\/ file:\/\/(.+)$/);
20
+ if (match) markers.push({ abs_path: match[1], joined_line: line });
21
+ if (nl === -1) break;
22
+ line += 1;
23
+ i = nl + 1;
24
+ }
25
+ return markers;
26
+ }
27
+
28
+ // Width of the token an error points at, so the caret can underline the whole
29
+ // identifier rather than a single column. Falls back to 1.
30
+ function token_width(line_text: string, start: number): number {
31
+ let width = 0;
32
+ while (start + width < line_text.length && /\w/.test(line_text[start + width])) {
33
+ width += 1;
34
+ }
35
+ return width || 1;
36
+ }
37
+
38
+ const TAB_WIDTH = 4;
39
+
40
+ // Visual column (0-based) for a 1-based char column, expanding tabs.
41
+ function visual_start(line_text: string, col: number): number {
42
+ let v = 0;
43
+ for (let k = 0; k < col - 1 && k < line_text.length; k++) {
44
+ v += line_text[k] === "\t" ? TAB_WIDTH : 1;
45
+ }
46
+ return v;
47
+ }
48
+
49
+ export default function render_errors(source: string, errors: CompileError[]): string {
50
+ const lines = source.split("\n");
51
+ const markers = find_file_markers(source);
52
+
53
+ const blocks: string[] = [];
54
+ for (const error of errors) {
55
+ const line_text = lines[error.line - 1] ?? "";
56
+
57
+ // Map the joined-source line back to the originating file + in-file line.
58
+ let rel_path = "";
59
+ let file_line = error.line;
60
+ for (let m = markers.length - 1; m >= 0; m--) {
61
+ if (markers[m].joined_line < error.line) {
62
+ file_line = error.line - markers[m].joined_line;
63
+ rel_path = path.relative(process.cwd(), markers[m].abs_path) || markers[m].abs_path;
64
+ break;
65
+ }
66
+ }
67
+
68
+ const gutter = " ".repeat(String(file_line).length);
69
+ const col = Math.max(1, error.column);
70
+ const display_line = line_text.replace(/\t/g, " ".repeat(TAB_WIDTH));
71
+ const squiggle = "~".repeat(token_width(line_text, col - 1));
72
+
73
+ blocks.push(
74
+ [
75
+ `Error: ${error.message}`,
76
+ ` File: ${rel_path}:${file_line}:${col}`,
77
+ `${gutter} |`,
78
+ `${file_line} | ${display_line}`,
79
+ `${gutter} | ${" ".repeat(visual_start(line_text, col))}${squiggle}`,
80
+ ].join("\n"),
81
+ );
82
+ }
83
+
84
+ const label = errors.length === 1 ? "error" : "errors";
85
+ return `\n${blocks.join("\n\n")}\n\n${errors.length} ${label} found.\n`;
86
+ }
package/src/index.ts ADDED
@@ -0,0 +1,281 @@
1
+ #! /usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import chokidar from "chokidar";
7
+ import { hideBin } from "yargs/helpers";
8
+ import yargs from "yargs/yargs";
9
+
10
+ import build, { default_platform } from "../../src/build.ts";
11
+ import join from "../../src/join.ts";
12
+ import { get_library } from "../../src/lib.ts";
13
+ import parse from "../../src/parse.ts";
14
+ import render_errors from "./format_errors.ts";
15
+ import type Config from "./types/Config.ts";
16
+
17
+ const SUPPORTED_EXTENSION = ".nm";
18
+
19
+ console.log("\n~ NOMEN ~\n");
20
+
21
+ const options = yargs(hideBin(process.argv))
22
+ .usage("Usage: nomen --in [file/folder]")
23
+ .option("in", {
24
+ alias: "i",
25
+ describe: "Input file or folder",
26
+ type: "string",
27
+ })
28
+ .option("out", {
29
+ alias: "o",
30
+ describe: "Output file",
31
+ type: "string",
32
+ })
33
+ .option("config", {
34
+ alias: "c",
35
+ describe: "The path to a config file",
36
+ type: "string",
37
+ })
38
+ .option("watch", {
39
+ alias: "w",
40
+ describe: "Whether to watch for file changes",
41
+ type: "boolean",
42
+ })
43
+ .option("arch", {
44
+ alias: "a",
45
+ describe: "Target architecture (aarch64 or c)",
46
+ type: "string",
47
+ default: "aarch64",
48
+ })
49
+ .option("platform", {
50
+ alias: "p",
51
+ describe: "Target platform (macos, ios, linux, android, windows, web)",
52
+ type: "string",
53
+ })
54
+ .option("lib", {
55
+ alias: "l",
56
+ describe: "Path to System library directory (containing package.jsonc)",
57
+ type: "string",
58
+ })
59
+ .option("audit", {
60
+ describe: "Whether to audit the generated program for memory issues",
61
+ type: "boolean",
62
+ })
63
+ .option("audit-runtime", {
64
+ describe: "Path to audit_runtime.c, linked in when --audit is set",
65
+ type: "string",
66
+ })
67
+ .help(true)
68
+ .check((argv) => {
69
+ if (!argv._.length && !argv.in) {
70
+ throw new Error("Missing required argument: in");
71
+ }
72
+ return true;
73
+ })
74
+ .parseSync();
75
+
76
+ try {
77
+ if (!options.in) {
78
+ process.exit(0);
79
+ }
80
+
81
+ if (fs.existsSync(options.in)) {
82
+ let config: Config = { arch: "aarch64", platform: default_platform() };
83
+ // Load the config from a file
84
+ if (options.config && fs.existsSync(options.config)) {
85
+ config = JSON.parse(fs.readFileSync(options.config, "utf-8"));
86
+ }
87
+ // Overwrite with args
88
+ if (options.arch) config.arch = options.arch as "aarch64" | "c";
89
+ if (options.platform) config.platform = options.platform as string;
90
+ if (options.lib) config.lib = options.lib;
91
+ if (options.audit) config.audit = options.audit;
92
+ if (options["audit-runtime"]) config.audit_runtime = options["audit-runtime"];
93
+
94
+ // Is the --in path a folder
95
+ if (fs.lstatSync(options.in).isDirectory()) {
96
+ // Loop through files in the folder
97
+ //processFolder(options.in);
98
+ if (options.watch) {
99
+ watchPath(options.in, config);
100
+ } else {
101
+ processFolder(options.in, config);
102
+ }
103
+ } else {
104
+ // Process the supplied file
105
+ const extname = path.extname(options.in);
106
+ if (shouldProcessFile(options.in)) {
107
+ //processFile(options.in);
108
+ // NOTE: We get add notifications for all watched files immediately
109
+ // TODO: Is this the case on Windows etc too?
110
+ if (options.watch) {
111
+ watchPath(options.in, config);
112
+ } else {
113
+ processFile(options.in, config);
114
+ }
115
+ } else {
116
+ console.log("Unsupported file type: " + extname);
117
+ }
118
+ }
119
+ } else {
120
+ console.log("Path not found: " + options.in);
121
+ }
122
+ } catch (err) {
123
+ console.log("UH", err);
124
+ }
125
+
126
+ function resolve_lib(file_path: string): string | undefined {
127
+ let dir = path.dirname(path.resolve(file_path));
128
+ for (let i = 0; i < 20; i++) {
129
+ const config_path = path.join(dir, "package.jsonc");
130
+ if (fs.existsSync(config_path)) {
131
+ try {
132
+ const raw = fs.readFileSync(config_path, "utf8");
133
+ const json = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
134
+ const parsed = JSON.parse(json);
135
+ if (parsed.imports?.System) {
136
+ return path.resolve(dir, parsed.imports.System);
137
+ }
138
+ } catch {
139
+ // ignore malformed package.jsonc and keep searching
140
+ }
141
+ }
142
+ const lib_config = path.join(dir, "core", "package.jsonc");
143
+ if (fs.existsSync(lib_config)) return path.join(dir, "core");
144
+ const parent = path.dirname(dir);
145
+ if (parent === dir) break;
146
+ dir = parent;
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function resolve_audit_runtime(config: Config, input_path: string): string | undefined {
152
+ if (config.audit_runtime) {
153
+ const explicit = path.resolve(config.audit_runtime);
154
+ return fs.existsSync(explicit) ? explicit : undefined;
155
+ }
156
+ let dir = path.dirname(input_path);
157
+ for (let i = 0; i < 20; i++) {
158
+ const candidate = path.join(dir, "src", "audit_runtime.c");
159
+ if (fs.existsSync(candidate)) return candidate;
160
+ const parent = path.dirname(dir);
161
+ if (parent === dir) break;
162
+ dir = parent;
163
+ }
164
+ return undefined;
165
+ }
166
+
167
+ function compile_audit_runtime(config: Config, input_path: string, buildDir: string): string {
168
+ const runtime_src = resolve_audit_runtime(config, input_path);
169
+ if (!runtime_src) {
170
+ throw new Error(
171
+ "Audit enabled but audit_runtime.c was not found. Pass --audit-runtime <path/to/audit_runtime.c>.",
172
+ );
173
+ }
174
+ const audit_obj = path.join(buildDir, "audit_runtime.o");
175
+ execSync(`clang -c ${runtime_src} -o ${audit_obj}`);
176
+ return audit_obj;
177
+ }
178
+
179
+ function watchPath(p: string, config: Config) {
180
+ chokidar.watch(p).on("all", (event, filePath) => {
181
+ if (shouldProcessFile(filePath)) {
182
+ processFile(filePath, config);
183
+ }
184
+ });
185
+ }
186
+
187
+ function processFolder(folder: string, config: Config) {
188
+ const dir = fs.opendirSync(folder);
189
+ let dirent;
190
+ while ((dirent = dir.readSync()) !== null) {
191
+ if (shouldProcessFile(dirent.name)) {
192
+ processFile(path.join(folder, dirent.name), config);
193
+ // @ts-ignore
194
+ let _ = fs.watch;
195
+ }
196
+ }
197
+ dir.closeSync();
198
+ }
199
+
200
+ function shouldProcessFile(filename: string) {
201
+ return path.extname(filename) === SUPPORTED_EXTENSION;
202
+ }
203
+
204
+ function processFile(filename: string, config: Config) {
205
+ console.log("Processing", filename);
206
+
207
+ const arch = config.arch || "aarch64";
208
+ const platform = config.platform || default_platform();
209
+
210
+ const resolved = path.resolve(filename);
211
+ if (!config.lib) {
212
+ config.lib = resolve_lib(resolved);
213
+ }
214
+
215
+ let startTime = performance.now();
216
+
217
+ let input = join(path.resolve(filename), config.lib);
218
+ const library = config.lib ? get_library(config.lib) : undefined;
219
+ const parsed = parse(input, library);
220
+ // TODO: If verbose flag
221
+ // console.log("Parsed");
222
+
223
+ let errors = parsed.errors;
224
+ const ok = !errors.length;
225
+
226
+ if (!ok) {
227
+ console.log(render_errors(input, errors));
228
+ return;
229
+ }
230
+
231
+ // TODO: If verbose flag
232
+ // console.log("Built");
233
+ const result = build(parsed.root, { arch, platform, audit: config.audit });
234
+
235
+ if (result.errors && result.errors.length > 0) {
236
+ console.log(render_errors(input, result.errors));
237
+ return;
238
+ }
239
+
240
+ const dir = path.dirname(filename);
241
+ const basename = path.basename(filename, ".nm");
242
+ const buildDir = path.join(dir, "build");
243
+ if (!fs.existsSync(buildDir)) {
244
+ fs.mkdirSync(buildDir, { recursive: true });
245
+ }
246
+ const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
247
+ const headerfile = path.join(buildDir, "main.h");
248
+ const codefile = path.join(buildDir, basename + ext);
249
+ const outfile = path.join(buildDir, basename);
250
+ fs.writeFileSync(headerfile, result.headers);
251
+ fs.writeFileSync(codefile, result.code);
252
+
253
+ let companionfile: string | undefined;
254
+ if (result.companion) {
255
+ const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
256
+ companionfile = path.join(buildDir, basename + "_companion" + comp_ext);
257
+ fs.writeFileSync(companionfile, result.companion);
258
+ }
259
+
260
+ const compileTime = performance.now();
261
+ console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
262
+ console.log("");
263
+
264
+ startTime = performance.now();
265
+
266
+ const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : undefined;
267
+ let link_inputs = codefile;
268
+ if (companionfile) link_inputs += ` ${companionfile}`;
269
+ if (audit_obj) link_inputs += ` ${audit_obj}`;
270
+ const framework_flags =
271
+ platform === "macos" || platform === "ios"
272
+ ? " -framework CoreGraphics -framework Foundation -framework AppKit -lobjc"
273
+ : "";
274
+ execSync(`clang -o ${outfile} ${link_inputs}${framework_flags}`);
275
+ execSync(outfile, { stdio: "inherit" });
276
+
277
+ const runTime = performance.now();
278
+ console.log("");
279
+ console.log("");
280
+ console.log(`Completed in ${(runTime - startTime).toFixed(2)}ms`);
281
+ }
@@ -0,0 +1,7 @@
1
+ export default interface Config {
2
+ arch?: "c" | "aarch64";
3
+ platform?: string;
4
+ lib?: string;
5
+ audit?: boolean;
6
+ audit_runtime?: string;
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "lib": ["es2023"],
5
+ "moduleDetection": "force",
6
+ "module": "nodenext",
7
+ "moduleResolution": "nodenext",
8
+ "resolveJsonModule": true,
9
+ "types": ["node"],
10
+ "strict": true,
11
+ "noUnusedLocals": true,
12
+ "declaration": true,
13
+ "noEmit": true,
14
+ "allowImportingTsExtensions": true,
15
+ "esModuleInterop": true,
16
+ "isolatedModules": true,
17
+ "verbatimModuleSyntax": true,
18
+ "skipLibCheck": true
19
+ },
20
+ "include": ["src"]
21
+ }