exprforge 0.2.0 → 0.2.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.
@@ -0,0 +1,126 @@
1
+ // exprforge/emitters/zig.js
2
+ const Emitter = require("./base.js");
3
+
4
+ // Zig's reserved words plus the small set of extra identifiers this
5
+ // emitter's own output introduces (Result, the struct-suite convention
6
+ // below) -- same role as QB64_RESERVED in emitters/qb64.js. Zig is
7
+ // case-sensitive, unlike QB64/Fortran, so this checks exact names.
8
+ const ZIG_RESERVED = new Set([
9
+ "addrspace", "align", "allowzero", "and", "anyframe", "anytype", "asm", "async",
10
+ "await", "break", "callconv", "catch", "comptime", "const", "continue", "defer",
11
+ "else", "enum", "errdefer", "error", "export", "extern", "fn", "for", "if",
12
+ "inline", "noalias", "noinline", "nosuspend", "opaque", "or", "orelse", "packed",
13
+ "pub", "resume", "return", "linksection", "struct", "suspend", "switch", "test",
14
+ "threadlocal", "try", "union", "unreachable", "usingnamespace", "var", "volatile",
15
+ "while", "true", "false", "null", "undefined", "void", "type", "anyerror",
16
+ "std", "result",
17
+ ]);
18
+
19
+ function checkReservedNames(names) {
20
+ for (const name of names) {
21
+ if (ZIG_RESERVED.has(name)) {
22
+ throw new Error(
23
+ `emitter for .zig: "${name}" is a reserved Zig keyword/identifier and can't be used as a ` +
24
+ `function/variable/parameter name -- rename it (see ZIG_RESERVED in emitters/zig.js)`,
25
+ );
26
+ }
27
+ }
28
+ }
29
+
30
+ function mathFn(name) {
31
+ return (args) => `std.math.${name}(${args.join(", ")})`;
32
+ }
33
+
34
+ function builtin(name) {
35
+ return ([x]) => `@${name}(${x})`;
36
+ }
37
+
38
+ function capitalize(s) {
39
+ return s.charAt(0).toUpperCase() + s.slice(1);
40
+ }
41
+
42
+ const emitter = new Emitter({
43
+ ext: "zig",
44
+ // Every literal is forced through @as(f64, ...), not left bare --
45
+ // confirmed against a real compiler that a bare literal reaching a
46
+ // comptime-eligible call (e.g. std.math.sqrt(2.0) with no runtime f64
47
+ // operand anywhere) gets evaluated at Zig's extended comptime_float
48
+ // precision instead of truncated to an actual IEEE 754 double, giving
49
+ // a DIFFERENT (more precise, not less) result than every other target
50
+ // here. @as(f64, ...) forces real double-precision truncation before
51
+ // any arithmetic happens, regardless of whether the surrounding
52
+ // expression also involves a runtime variable.
53
+ formatNumber: (v) => `@as(f64, ${String(v)})`,
54
+ calls: {
55
+ // Compiler builtins (map to LLVM intrinsics directly).
56
+ abs: builtin("abs"), exp: builtin("exp"), log: builtin("log"),
57
+ log2: builtin("log2"), log10: builtin("log10"),
58
+ floor: builtin("floor"), ceil: builtin("ceil"), round: builtin("round"), trunc: builtin("trunc"),
59
+ min: builtin2("min"), max: builtin2("max"),
60
+ // std.math functions -- not compiler builtins, but ordinary Zig
61
+ // stdlib functions with the expected 1/2-arg float signatures.
62
+ sqrt: mathFn("sqrt"), sin: mathFn("sin"), cos: mathFn("cos"), tan: mathFn("tan"),
63
+ asin: mathFn("asin"), acos: mathFn("acos"), atan: mathFn("atan"), atan2: mathFn("atan2"),
64
+ hypot: mathFn("hypot"),
65
+ // pow needs an explicit type argument (std.math.pow(T, x, y)) --
66
+ // unlike every other std.math function here, it's not purely
67
+ // inferred from its float arguments.
68
+ pow: ([x, y]) => `std.math.pow(f64, ${x}, ${y})`,
69
+ // std.math.sign is genuinely zero-aware (confirmed: sign(0.0) ==
70
+ // 0.0) -- unlike most other targets here, this doesn't need to be
71
+ // hand-built to get the zero case right (see Go's/Rust's sign()
72
+ // history in this project for what happens when a language's
73
+ // native sign function isn't).
74
+ sign: mathFn("sign"),
75
+ },
76
+ // Zig has no ?: ternary, but if/else as an EXPRESSION (not just a
77
+ // statement) works exactly like this project's select() -- including
78
+ // evaluating both branches, since there's no separate "lazy" form.
79
+ emitSelect: function (condNode, thenStr, elseStr) {
80
+ const L = this.emitExpr(condNode.left);
81
+ const R = this.emitExpr(condNode.right);
82
+ return `(if ((${L}) ${condNode.op} (${R})) ${thenStr} else ${elseStr})`;
83
+ },
84
+ formatFunction: (fn, body, letBindings = []) => {
85
+ checkReservedNames([fn.name, ...fn.params, ...letBindings.map((b) => b.name)]);
86
+ const params = fn.params.map((p) => `${p}: f64`).join(", ");
87
+ const lets = letBindings.map(({ name, valueStr }) => ` const ${name}: f64 = ${valueStr};`).join("\n");
88
+ const letsBlock = lets ? lets + "\n" : "";
89
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
90
+ `const std = @import("std");\n\n` +
91
+ `pub fn ${fn.name}(${params}) f64 {\n` +
92
+ letsBlock +
93
+ ` return ${body};\n` +
94
+ `}\n`;
95
+ },
96
+ // Multiple named outputs from one call: a small struct type (same idea
97
+ // as C's typedef struct, Rust's struct) returned by value -- Zig has
98
+ // no native tuple-with-names or multi-return.
99
+ formatSuite: (fn, outputStrs, letBindings = []) => {
100
+ const outputNames = Object.keys(outputStrs);
101
+ checkReservedNames([fn.name, ...fn.params, ...outputNames, ...letBindings.map((b) => b.name)]);
102
+ const resultName = `${capitalize(fn.name)}Result`;
103
+ const params = fn.params.map((p) => `${p}: f64`).join(", ");
104
+ const lets = letBindings.map(({ name, valueStr }) => ` const ${name}: f64 = ${valueStr};`).join("\n");
105
+ const letsBlock = lets ? lets + "\n" : "";
106
+ const structFields = outputNames.map((n) => ` ${n}: f64,`).join("\n");
107
+ const initFields = outputNames.map((n) => ` .${n} = ${outputStrs[n]},`).join("\n");
108
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
109
+ `const std = @import("std");\n\n` +
110
+ `pub const ${resultName} = struct {\n${structFields}\n};\n\n` +
111
+ `pub fn ${fn.name}(${params}) ${resultName} {\n` +
112
+ letsBlock +
113
+ ` return ${resultName}{\n${initFields}\n };\n` +
114
+ `}\n`;
115
+ },
116
+ });
117
+
118
+ // builtin2 is used by the calls table above but defined after it's
119
+ // referenced (function declarations hoist) -- kept next to builtin/mathFn
120
+ // rather than above them so the two "2-arg compiler builtin" and "1-arg
121
+ // compiler builtin" helpers read as a pair.
122
+ function builtin2(name) {
123
+ return ([a, b]) => `@${name}(${a}, ${b})`;
124
+ }
125
+
126
+ module.exports = emitter;
package/math/index.js CHANGED
@@ -67,6 +67,14 @@ function cross3(ax, ay, az, bx, by, bz) {
67
67
  // with another normalize3() binding inside the same function body, and a
68
68
  // process-wide counter trivially guarantees that regardless of how many
69
69
  // times normalize3 is called across however many functions.
70
+ //
71
+ // The name itself starts with a letter, not "__" -- confirmed against a
72
+ // real Fortran compiler ("Invalid character in name") that a leading
73
+ // underscore isn't a valid identifier start there, unlike JS/Python/etc.
74
+ // This is an internal implementation detail (never part of any documented
75
+ // return value or public name), so there's nothing for a leading-"__"
76
+ // convention to usefully signal here that a portable identifier can't
77
+ // signal just as well.
70
78
  let normalizeGensymCounter = 0;
71
79
 
72
80
  // Safe-normalize a 3-D vector. Returns { x, y, z } (same shape as cross3).
@@ -85,7 +93,7 @@ let normalizeGensymCounter = 0;
85
93
  // name avoids a "duplicate let binding name" throw if normalize3 is called
86
94
  // more than once inside one function (e.g. normalizing two vectors).
87
95
  function normalize3(x, y, z, fx = num(0), fy = num(1), fz = num(0)) {
88
- const lenName = `__exprforgeMathNrmLen${normalizeGensymCounter++}`;
96
+ const lenName = `efMathNrmLen${normalizeGensymCounter++}`;
89
97
  return {
90
98
  x: letIn(lenName, len3(x, y, z), safeDiv(x, v(lenName), fx)),
91
99
  y: safeDiv(y, v(lenName), fy),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exprforge",
3
- "version": "0.2.0",
4
- "description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, and Rust.",
3
+ "version": "0.2.1",
4
+ "description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "exports": {
@@ -10,17 +10,21 @@
10
10
  const { v, num, outputs } = require("../ast.js");
11
11
  const { safeDiv, dot3, len3, cross3, normalize3, clamp } = require("../math/index.js");
12
12
 
13
- const MATH_DEMO_PARAMS = ["ax", "ay", "az", "bx", "by", "bz", "t", "lo", "hi"];
13
+ // "byy", not "by": BY is a reserved COBOL keyword (BY REFERENCE/BY VALUE/
14
+ // BY CONTENT) -- confirmed against a real compiler ("syntax error,
15
+ // unexpected BY") that it can't be used as a data-item name there, same
16
+ // kind of unavoidable per-language collision as "len"/"mag" below.
17
+ const MATH_DEMO_PARAMS = ["ax", "ay", "az", "bx", "byy", "bz", "t", "lo", "hi"];
14
18
 
15
- const cross = cross3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz"));
19
+ const cross = cross3(v("ax"), v("ay"), v("az"), v("bx"), v("byy"), v("bz"));
16
20
  const normA = normalize3(v("ax"), v("ay"), v("az"));
17
- const normB = normalize3(v("bx"), v("by"), v("bz"));
21
+ const normB = normalize3(v("bx"), v("byy"), v("bz"));
18
22
 
19
23
  const MathDemo = {
20
24
  name: "MathDemo",
21
25
  params: MATH_DEMO_PARAMS,
22
26
  body: outputs({
23
- dot: dot3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz")),
27
+ dot: dot3(v("ax"), v("ay"), v("az"), v("bx"), v("byy"), v("bz")),
24
28
  // "mag", not "len": LEN is a reserved QB64 builtin (string/array
25
29
  // length) -- see test/conformance.test.js's normalizeXAst comment.
26
30
  mag: len3(v("ax"), v("ay"), v("az")),