@hyperscale0/hsx 1.0.0-alpha.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.
Files changed (64) hide show
  1. package/AUTHORS +8 -0
  2. package/CHANGELOG.md +59 -0
  3. package/LICENSE +661 -0
  4. package/LICENSING.md +52 -0
  5. package/README.md +170 -0
  6. package/SECURITY.md +47 -0
  7. package/TRADEMARKS.md +35 -0
  8. package/bin/hsx.ts +15 -0
  9. package/dist/bin/hsx.d.ts +7 -0
  10. package/dist/bin/hsx.d.ts.map +1 -0
  11. package/dist/bin/hsx.js +14 -0
  12. package/dist/bin/hsx.js.map +1 -0
  13. package/dist/src/ast.d.ts +172 -0
  14. package/dist/src/ast.d.ts.map +1 -0
  15. package/dist/src/ast.js +22 -0
  16. package/dist/src/ast.js.map +1 -0
  17. package/dist/src/check.d.ts +11 -0
  18. package/dist/src/check.d.ts.map +1 -0
  19. package/dist/src/check.js +1214 -0
  20. package/dist/src/check.js.map +1 -0
  21. package/dist/src/cli.d.ts +20 -0
  22. package/dist/src/cli.d.ts.map +1 -0
  23. package/dist/src/cli.js +137 -0
  24. package/dist/src/cli.js.map +1 -0
  25. package/dist/src/compile.d.ts +39 -0
  26. package/dist/src/compile.d.ts.map +1 -0
  27. package/dist/src/compile.js +59 -0
  28. package/dist/src/compile.js.map +1 -0
  29. package/dist/src/index.d.ts +9 -0
  30. package/dist/src/index.d.ts.map +1 -0
  31. package/dist/src/index.js +7 -0
  32. package/dist/src/index.js.map +1 -0
  33. package/dist/src/lex.d.ts +23 -0
  34. package/dist/src/lex.d.ts.map +1 -0
  35. package/dist/src/lex.js +125 -0
  36. package/dist/src/lex.js.map +1 -0
  37. package/dist/src/lower.d.ts +93 -0
  38. package/dist/src/lower.d.ts.map +1 -0
  39. package/dist/src/lower.js +2081 -0
  40. package/dist/src/lower.js.map +1 -0
  41. package/dist/src/model.d.ts +307 -0
  42. package/dist/src/model.d.ts.map +1 -0
  43. package/dist/src/model.js +15 -0
  44. package/dist/src/model.js.map +1 -0
  45. package/dist/src/parse.d.ts +19 -0
  46. package/dist/src/parse.d.ts.map +1 -0
  47. package/dist/src/parse.js +484 -0
  48. package/dist/src/parse.js.map +1 -0
  49. package/dist/src/version.d.ts +16 -0
  50. package/dist/src/version.d.ts.map +1 -0
  51. package/dist/src/version.js +16 -0
  52. package/dist/src/version.js.map +1 -0
  53. package/package.json +79 -0
  54. package/spec/hsx-ir.schema.json +522 -0
  55. package/src/ast.ts +231 -0
  56. package/src/check.ts +1699 -0
  57. package/src/cli.ts +173 -0
  58. package/src/compile.ts +98 -0
  59. package/src/index.ts +16 -0
  60. package/src/lex.ts +161 -0
  61. package/src/lower.ts +2619 -0
  62. package/src/model.ts +340 -0
  63. package/src/parse.ts +580 -0
  64. package/src/version.ts +17 -0
package/src/cli.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The `hsx` command line: two subcommands over the one compiler entry point.
3
+ *
4
+ * `check` prints diagnostics and says nothing else; `build` writes the
5
+ * compiled artifacts as JSON. Neither reads the environment, neither touches
6
+ * a network, and neither writes outside the path the caller names.
7
+ *
8
+ * Everything here is a pure function of `argv` plus the injected `Io`, so the
9
+ * spec drives the real code paths with an in-memory filesystem instead of
10
+ * asserting on a subprocess's scrollback.
11
+ */
12
+
13
+ import { compile, type CompileResult } from "./compile.ts";
14
+ import { HSX_IR_VERSION, HSX_VERSION } from "./version.ts";
15
+
16
+ /** Filesystem and streams, injected so the CLI stays testable. */
17
+ export interface Io {
18
+ readonly err: (line: string) => void;
19
+ readonly out: (line: string) => void;
20
+ readonly readFile: (path: string) => Promise<string>;
21
+ readonly writeFile: (path: string, contents: string) => Promise<void>;
22
+ }
23
+
24
+ /**
25
+ * Exit codes. `warning` exits 0 because a lint note is not a failure; pass
26
+ * `--strict` to make it one.
27
+ */
28
+ const OK = 0;
29
+ const REFUSED = 1;
30
+ const USAGE = 2;
31
+
32
+ const USAGE_TEXT = `hsx ${HSX_VERSION}, the HSX compiler
33
+
34
+ Usage:
35
+ hsx check <file.hsx> [--strict]
36
+ hsx build <file.hsx> [--out <file.json>] [--strict]
37
+ hsx --version
38
+ hsx --help
39
+
40
+ Commands:
41
+ check Compile and report diagnostics. Prints nothing when the program is clean.
42
+ build Compile and write the HSX-JSON IR document and Business Frame as JSON.
43
+
44
+ Options:
45
+ --out <file> Write build output to this path instead of stdout.
46
+ --strict Treat warning-severity diagnostics as failures.
47
+
48
+ Exit codes:
49
+ 0 the program compiled (verdict valid, or warning without --strict)
50
+ 1 the program was refused (verdict invalid, or warning with --strict)
51
+ 2 the command line or the input file could not be used`;
52
+
53
+ export async function runCli(argv: readonly string[], io: Io): Promise<number> {
54
+ const [command, ...rest] = argv;
55
+
56
+ if (command === undefined || command === "--help" || command === "-h") {
57
+ io.out(USAGE_TEXT);
58
+ return command === undefined ? USAGE : OK;
59
+ }
60
+ if (command === "--version" || command === "-v") {
61
+ io.out(`${HSX_VERSION} (IR version ${HSX_IR_VERSION})`);
62
+ return OK;
63
+ }
64
+ if (command !== "check" && command !== "build") {
65
+ io.err(`hsx: unknown command "${command}"`);
66
+ io.err(USAGE_TEXT);
67
+ return USAGE;
68
+ }
69
+
70
+ const parsed = parseOptions(rest, command);
71
+ if ("error" in parsed) {
72
+ io.err(`hsx: ${parsed.error}`);
73
+ return USAGE;
74
+ }
75
+
76
+ let source: string;
77
+ try {
78
+ source = await io.readFile(parsed.file);
79
+ } catch (cause) {
80
+ io.err(`hsx: cannot read ${parsed.file}: ${messageOf(cause)}`);
81
+ return USAGE;
82
+ }
83
+
84
+ const result = compile(source);
85
+ for (const line of diagnosticLines(parsed.file, result)) io.err(line);
86
+
87
+ const refused =
88
+ result.verdict === "invalid" ||
89
+ (parsed.strict && result.verdict === "warning");
90
+
91
+ if (command === "check") {
92
+ return refused ? REFUSED : OK;
93
+ }
94
+
95
+ if (!result.artifacts) return REFUSED;
96
+ const json = `${JSON.stringify(
97
+ {
98
+ document: result.artifacts.document,
99
+ frame: result.artifacts.frame,
100
+ },
101
+ null,
102
+ 2,
103
+ )}\n`;
104
+ if (parsed.out === undefined) {
105
+ io.out(json.trimEnd());
106
+ } else {
107
+ try {
108
+ await io.writeFile(parsed.out, json);
109
+ } catch (cause) {
110
+ io.err(`hsx: cannot write ${parsed.out}: ${messageOf(cause)}`);
111
+ return USAGE;
112
+ }
113
+ }
114
+ return refused ? REFUSED : OK;
115
+ }
116
+
117
+ interface Options {
118
+ readonly file: string;
119
+ readonly out?: string;
120
+ readonly strict: boolean;
121
+ }
122
+
123
+ function parseOptions(
124
+ args: readonly string[],
125
+ command: "build" | "check",
126
+ ): Options | { readonly error: string } {
127
+ let file: string | undefined;
128
+ let out: string | undefined;
129
+ let strict = false;
130
+
131
+ for (let index = 0; index < args.length; index += 1) {
132
+ const argument = args[index] as string;
133
+ if (argument === "--strict") {
134
+ strict = true;
135
+ continue;
136
+ }
137
+ if (argument === "--out") {
138
+ if (command !== "build") return { error: "--out belongs to hsx build" };
139
+ const value = args[index + 1];
140
+ if (value === undefined || value.startsWith("--")) {
141
+ return { error: "--out needs a file path" };
142
+ }
143
+ out = value;
144
+ index += 1;
145
+ continue;
146
+ }
147
+ if (argument.startsWith("-")) {
148
+ return { error: `unknown option "${argument}"` };
149
+ }
150
+ if (file !== undefined) {
151
+ return { error: `hsx ${command} takes one file, got "${argument}" too` };
152
+ }
153
+ file = argument;
154
+ }
155
+
156
+ if (file === undefined) return { error: `hsx ${command} needs a file` };
157
+ return { file, ...(out === undefined ? {} : { out }), strict };
158
+ }
159
+
160
+ /** `file:line:col: severity [stage] message`, the shape editors already parse. */
161
+ function diagnosticLines(
162
+ file: string,
163
+ result: CompileResult,
164
+ ): readonly string[] {
165
+ return result.diagnostics.map(
166
+ (diagnostic) =>
167
+ `${file}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.severity} [${diagnostic.stage}] ${diagnostic.message}`,
168
+ );
169
+ }
170
+
171
+ function messageOf(cause: unknown): string {
172
+ return cause instanceof Error ? cause.message : String(cause);
173
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The HSX compiler driver: source text in, three-verdict result out.
3
+ *
4
+ * - `valid`: the program lowers cleanly; the IR document and frame are ready
5
+ * for the independent checker.
6
+ * - `warning`: the program lowers, and the compiler's lint voice has notes
7
+ * the author should read (the artifacts are still present and usable).
8
+ * - `invalid`: the program cannot be lowered; diagnostics say why, in the
9
+ * author's language, each anchored to a source line and column.
10
+ *
11
+ * Every diagnostic points at SOURCE coordinates, never at lowered IR paths.
12
+ */
13
+
14
+ import { lineColAt } from "./ast.ts";
15
+ import { checkProgram } from "./check.ts";
16
+ import { lowerProgram } from "./lower.ts";
17
+ import { parseProgram } from "./parse.ts";
18
+
19
+ type Json = Record<string, unknown>;
20
+
21
+ type CompileVerdict = "invalid" | "valid" | "warning";
22
+
23
+ interface CompileDiagnostic {
24
+ /** 1-indexed source column. */
25
+ readonly column: number;
26
+ /** 1-indexed source line. */
27
+ readonly line: number;
28
+ readonly message: string;
29
+ readonly severity: "error" | "warning";
30
+ /** The stage that raised it: parse, check, or lower. */
31
+ readonly stage: "check" | "lower" | "parse";
32
+ }
33
+
34
+ interface CompileArtifacts {
35
+ /** The lowered HSX-JSON IR document, for the independent checker. */
36
+ readonly document: Json;
37
+ /** The congruent Business Frame. */
38
+ readonly frame: Json;
39
+ }
40
+
41
+ export interface CompileResult {
42
+ /** Present exactly when the verdict is not `invalid`. */
43
+ readonly artifacts?: CompileArtifacts;
44
+ readonly diagnostics: readonly CompileDiagnostic[];
45
+ readonly verdict: CompileVerdict;
46
+ }
47
+
48
+ export function compile(source: string): CompileResult {
49
+ const diagnostics: CompileDiagnostic[] = [];
50
+ const at = (
51
+ stage: CompileDiagnostic["stage"],
52
+ severity: CompileDiagnostic["severity"],
53
+ message: string,
54
+ offset: number,
55
+ ): void => {
56
+ const position = lineColAt(source, offset);
57
+ diagnostics.push({
58
+ column: position.column,
59
+ line: position.line,
60
+ message,
61
+ severity,
62
+ stage,
63
+ });
64
+ };
65
+
66
+ const parsed = parseProgram(source);
67
+ for (const diagnostic of parsed.diagnostics) {
68
+ at("parse", "error", diagnostic.message, diagnostic.span.start);
69
+ }
70
+ if (parsed.diagnostics.length > 0) {
71
+ return { diagnostics, verdict: "invalid" };
72
+ }
73
+
74
+ const checked = checkProgram(parsed.program);
75
+ for (const diagnostic of checked.diagnostics) {
76
+ at("check", diagnostic.severity, diagnostic.message, diagnostic.span.start);
77
+ }
78
+ if (!checked.program) {
79
+ return { diagnostics, verdict: "invalid" };
80
+ }
81
+
82
+ const loweredResult = lowerProgram(checked.program);
83
+ if (!loweredResult.ok) {
84
+ for (const issue of loweredResult.issues) {
85
+ at("lower", "error", issue.message, issue.span.start);
86
+ }
87
+ return { diagnostics, verdict: "invalid" };
88
+ }
89
+
90
+ return {
91
+ artifacts: {
92
+ document: loweredResult.value.document,
93
+ frame: loweredResult.value.frame,
94
+ },
95
+ diagnostics,
96
+ verdict: diagnostics.length > 0 ? "warning" : "valid",
97
+ };
98
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type {
2
+ BlockExpr,
3
+ CallExpr,
4
+ ListExpr,
5
+ PercentExpr,
6
+ PortDecl,
7
+ PortRefExpr,
8
+ SettlementDecl,
9
+ } from "./ast.ts";
10
+ export { lineColAt } from "./ast.ts";
11
+ export { checkProgram } from "./check.ts";
12
+ export { compile } from "./compile.ts";
13
+ export type { CompileResult } from "./compile.ts";
14
+ export { lowerProgram, MONEY_EVENT_BUDGET } from "./lower.ts";
15
+ export { parseProgram } from "./parse.ts";
16
+ export { HSX_IR_VERSION, HSX_VERSION } from "./version.ts";
package/src/lex.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The HSX lexer. Hand-written, total: every input produces a token stream
3
+ * ending in `eof`, with malformed stretches reported as diagnostics and
4
+ * skipped. Tokens carry byte-offset spans; whitespace and `//` comments are
5
+ * insignificant everywhere.
6
+ */
7
+
8
+ import type { Diagnostic, Span } from "./ast.ts";
9
+
10
+ type TokenKind =
11
+ | "eof"
12
+ | "ident"
13
+ | "keyword"
14
+ | "number"
15
+ | "percent"
16
+ | "punct"
17
+ | "string";
18
+
19
+ const KEYWORDS = [
20
+ "asset",
21
+ "from",
22
+ "import",
23
+ "party",
24
+ "port",
25
+ "program",
26
+ "settlement",
27
+ ] as const;
28
+
29
+ export interface Token {
30
+ readonly kind: TokenKind;
31
+ readonly span: Span;
32
+ /** Identifier name, keyword, punctuation glyph, or raw literal text. */
33
+ readonly text: string;
34
+ /** Decoded value for string literals; raw digits for numbers/percents. */
35
+ readonly value: string;
36
+ }
37
+
38
+ const PUNCT = new Set(["{", "}", "(", ")", "[", "]", ":", ",", "=", "|", "."]);
39
+ const KEYWORD_SET: ReadonlySet<string> = new Set(KEYWORDS);
40
+
41
+ // Case conventions (snake_case declarations, uppercase currency codes) are
42
+ // semantic rules the typechecker words per position; the lexer stays permissive.
43
+ const isIdentStart = (ch: string): boolean => /[A-Za-z]/.test(ch);
44
+ const isIdentPart = (ch: string): boolean => /[A-Za-z0-9_]/.test(ch);
45
+ const isDigit = (ch: string): boolean => ch >= "0" && ch <= "9";
46
+
47
+ export interface LexResult {
48
+ readonly diagnostics: readonly Diagnostic[];
49
+ readonly tokens: readonly Token[];
50
+ }
51
+
52
+ export function lex(source: string): LexResult {
53
+ const tokens: Token[] = [];
54
+ const diagnostics: Diagnostic[] = [];
55
+ let index = 0;
56
+
57
+ const push = (kind: TokenKind, start: number, text: string, value = text) => {
58
+ tokens.push({ kind, span: { end: index, start }, text, value });
59
+ };
60
+
61
+ while (index < source.length) {
62
+ const ch = source[index] as string;
63
+
64
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
65
+ index += 1;
66
+ continue;
67
+ }
68
+
69
+ if (ch === "/" && source[index + 1] === "/") {
70
+ while (index < source.length && source[index] !== "\n") index += 1;
71
+ continue;
72
+ }
73
+
74
+ if (PUNCT.has(ch)) {
75
+ const start = index;
76
+ index += 1;
77
+ push("punct", start, ch);
78
+ continue;
79
+ }
80
+
81
+ if (ch === '"') {
82
+ const start = index;
83
+ index += 1;
84
+ let value = "";
85
+ let closed = false;
86
+ while (index < source.length) {
87
+ const next = source[index] as string;
88
+ if (next === '"') {
89
+ index += 1;
90
+ closed = true;
91
+ break;
92
+ }
93
+ if (next === "\n") break;
94
+ value += next;
95
+ index += 1;
96
+ }
97
+ if (!closed) {
98
+ diagnostics.push({
99
+ message: "this string never closes; add the ending double quote",
100
+ span: { end: index, start },
101
+ });
102
+ }
103
+ push("string", start, source.slice(start, index), value);
104
+ continue;
105
+ }
106
+
107
+ if (isDigit(ch)) {
108
+ const start = index;
109
+ while (index < source.length && isDigit(source[index] as string)) {
110
+ index += 1;
111
+ }
112
+ if (source[index] === "." && isDigit(source[index + 1] ?? "")) {
113
+ index += 1;
114
+ while (index < source.length && isDigit(source[index] as string)) {
115
+ index += 1;
116
+ }
117
+ }
118
+ const raw = source.slice(start, index);
119
+ if (source[index] === "%") {
120
+ index += 1;
121
+ push("percent", start, source.slice(start, index), raw);
122
+ } else {
123
+ push("number", start, raw, raw);
124
+ }
125
+ continue;
126
+ }
127
+
128
+ if (isIdentStart(ch)) {
129
+ const start = index;
130
+ while (index < source.length && isIdentPart(source[index] as string)) {
131
+ index += 1;
132
+ }
133
+ const text = source.slice(start, index);
134
+ push(KEYWORD_SET.has(text) ? "keyword" : "ident", start, text);
135
+ continue;
136
+ }
137
+
138
+ const start = index;
139
+ while (
140
+ index < source.length &&
141
+ !/[\sA-Za-z0-9_"]/.test(source[index] as string) &&
142
+ !PUNCT.has(source[index] as string) &&
143
+ !(source[index] === "/" && source[index + 1] === "/")
144
+ ) {
145
+ index += 1;
146
+ }
147
+ if (index === start) index += 1;
148
+ diagnostics.push({
149
+ message: `"${source.slice(start, index)}" is not part of the HSX language`,
150
+ span: { end: index, start },
151
+ });
152
+ }
153
+
154
+ tokens.push({
155
+ kind: "eof",
156
+ span: { end: source.length, start: source.length },
157
+ text: "",
158
+ value: "",
159
+ });
160
+ return { diagnostics, tokens };
161
+ }