numbl 0.0.4 → 0.0.6

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 (3) hide show
  1. package/README.md +26 -0
  2. package/dist-cli/cli.js +194 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,6 +26,32 @@ numbl # interactive REPL
26
26
  numbl --info # check native addon status
27
27
  ```
28
28
 
29
+ ## Try it with Docker
30
+
31
+ Run the REPL instantly — no installation required:
32
+
33
+ ```bash
34
+ docker run -it node:22 npx numbl
35
+ ```
36
+
37
+ With plotting support (open http://localhost:8234 in your browser):
38
+
39
+ ```bash
40
+ docker run -it -p 8234:8234 node:22 npx numbl --plot-port 8234
41
+ ```
42
+
43
+ With native LAPACK support for fast linear algebra:
44
+
45
+ ```bash
46
+ docker run -it node:22 bash -c "apt-get update && apt-get install -y libopenblas-dev && npx numbl build-addon && npx numbl"
47
+ ```
48
+
49
+ With both LAPACK and plotting:
50
+
51
+ ```bash
52
+ docker run -it -p 8234:8234 node:22 bash -c "apt-get update && apt-get install -y libopenblas-dev && npx numbl build-addon && npx numbl --plot-port 8234"
53
+ ```
54
+
29
55
  ## Upgrading
30
56
 
31
57
  ```bash
package/dist-cli/cli.js CHANGED
@@ -202,6 +202,21 @@ function shareRuntimeValue(v) {
202
202
  _rc: v._rc
203
203
  };
204
204
  }
205
+ if (v.kind === "class_instance" && !v.isHandleClass) {
206
+ const newFields = /* @__PURE__ */ new Map();
207
+ for (const [k, fv] of v.fields) {
208
+ newFields.set(k, shareRuntimeValue(fv));
209
+ }
210
+ const copy = {
211
+ kind: "class_instance",
212
+ className: v.className,
213
+ fields: newFields,
214
+ isHandleClass: false
215
+ };
216
+ if (v.$m) copy.$m = v.$m;
217
+ if (v._builtinData) copy._builtinData = shareRuntimeValue(v._builtinData);
218
+ return copy;
219
+ }
205
220
  return v;
206
221
  }
207
222
 
@@ -12679,6 +12694,13 @@ function indexIntoRTValue(base, indices) {
12679
12694
  return base;
12680
12695
  }
12681
12696
  }
12697
+ if (base.kind === "struct" || base.kind === "class_instance") {
12698
+ if (indices.length === 1) {
12699
+ const i = Math.round(toNumber(indices[0]));
12700
+ if (i !== 1) throw new RuntimeError("Index exceeds struct dimensions");
12701
+ return base;
12702
+ }
12703
+ }
12682
12704
  if (base.kind === "struct_array") {
12683
12705
  if (indices.length === 1) {
12684
12706
  const idx = indices[0];
@@ -12717,6 +12739,12 @@ function storeIntoRTValueIndex(base, indices, rhs) {
12717
12739
  const idx = indices[0];
12718
12740
  if (idx.kind === "number") {
12719
12741
  toDelete.add(Math.round(idx.value) - 1);
12742
+ } else if (idx.kind === "logical") {
12743
+ if (idx.value) toDelete.add(0);
12744
+ } else if (idx.kind === "tensor" && idx._isLogical) {
12745
+ for (let i = 0; i < idx.data.length; i++) {
12746
+ if (idx.data[i] !== 0) toDelete.add(i);
12747
+ }
12720
12748
  } else if (idx.kind === "tensor") {
12721
12749
  for (let i = 0; i < idx.data.length; i++) {
12722
12750
  toDelete.add(Math.round(idx.data[i]) - 1);
@@ -12743,6 +12771,12 @@ function storeIntoRTValueIndex(base, indices, rhs) {
12743
12771
  const s = /* @__PURE__ */ new Set();
12744
12772
  if (idx.kind === "number") {
12745
12773
  s.add(Math.round(idx.value) - 1);
12774
+ } else if (idx.kind === "logical") {
12775
+ if (idx.value) s.add(0);
12776
+ } else if (idx.kind === "tensor" && idx._isLogical) {
12777
+ for (let i = 0; i < idx.data.length; i++) {
12778
+ if (idx.data[i] !== 0) s.add(i);
12779
+ }
12746
12780
  } else if (idx.kind === "tensor") {
12747
12781
  for (let i = 0; i < idx.data.length; i++)
12748
12782
  s.add(Math.round(idx.data[i]) - 1);
@@ -13281,6 +13315,26 @@ function storeIntoRTValueIndex(base, indices, rhs) {
13281
13315
  base = RTV.cell([...base.data], [...base.shape]);
13282
13316
  }
13283
13317
  if (indices.length === 1) {
13318
+ if (indices[0].kind === "tensor" && !indices[0]._isLogical) {
13319
+ const idx = indices[0];
13320
+ if (rhs.kind !== "cell" || rhs.data.length !== idx.data.length) {
13321
+ throw new RuntimeError("Subscripted assignment dimension mismatch");
13322
+ }
13323
+ let maxIdx = -1;
13324
+ for (let j = 0; j < idx.data.length; j++) {
13325
+ const pos = Math.round(idx.data[j]) - 1;
13326
+ if (pos < 0) throw new RuntimeError("Cell index exceeds bounds");
13327
+ if (pos > maxIdx) maxIdx = pos;
13328
+ }
13329
+ while (base.data.length <= maxIdx)
13330
+ base.data.push(RTV.tensor(new FloatXArray(0), [0, 0]));
13331
+ for (let j = 0; j < idx.data.length; j++) {
13332
+ const pos = Math.round(idx.data[j]) - 1;
13333
+ base.data[pos] = rhs.data[j];
13334
+ }
13335
+ base.shape = [1, base.data.length];
13336
+ return base;
13337
+ }
13284
13338
  const i = Math.round(toNumber(indices[0])) - 1;
13285
13339
  if (i < 0) throw new RuntimeError("Cell index exceeds bounds");
13286
13340
  if (i >= base.data.length) {
@@ -16718,8 +16772,14 @@ function elemwise(fn, o) {
16718
16772
  const apply = (args) => {
16719
16773
  if (args.length !== 1) throw new RuntimeError(`Expected 1 argument`);
16720
16774
  const v = args[0];
16721
- if (v.kind === "number") return RTV.num(fn(v.value));
16722
- if (v.kind === "logical") return RTV.num(fn(v.value ? 1 : 0));
16775
+ if (v.kind === "number") {
16776
+ const result = fn(v.value);
16777
+ return o?.isLogical ? RTV.logical(!!result) : RTV.num(result);
16778
+ }
16779
+ if (v.kind === "logical") {
16780
+ const result = fn(v.value ? 1 : 0);
16781
+ return o?.isLogical ? RTV.logical(!!result) : RTV.num(result);
16782
+ }
16723
16783
  if (v.kind === "tensor") {
16724
16784
  const result = new FloatXArray(v.data.length);
16725
16785
  for (let i = 0; i < v.data.length; i++) result[i] = fn(v.data[i]);
@@ -16731,14 +16791,18 @@ function elemwise(fn, o) {
16731
16791
  throw new RuntimeError("Expected numeric argument");
16732
16792
  };
16733
16793
  if (o?.nativeJs) {
16794
+ const outKind = o.isLogical ? "Logical" : "Num";
16734
16795
  ret.push({
16735
16796
  check: (argTypes) => {
16736
16797
  if (argTypes.length !== 1) return null;
16737
16798
  if (argTypes[0].kind !== "Num") return null;
16738
- return { outputTypes: [{ kind: "Num" }] };
16799
+ return { outputTypes: [{ kind: outKind }] };
16739
16800
  },
16740
16801
  apply,
16741
- nativeJsFn: o.nativeJs
16802
+ // Disable native JS fast-path for logical builtins — their scalar
16803
+ // results must stay as RuntimeValue logicals so the codegen emits
16804
+ // $rt.not() instead of the `=== 0` fast path.
16805
+ nativeJsFn: o.isLogical ? void 0 : o.nativeJs
16742
16806
  });
16743
16807
  }
16744
16808
  ret.push({
@@ -17311,20 +17375,17 @@ function registerArrayFunctions() {
17311
17375
  }
17312
17376
  }
17313
17377
  ]);
17314
- register("NaN", [
17315
- {
17316
- check: arrayConstructorCheck,
17317
- apply: (args) => {
17318
- if (args.length === 0) return RTV.num(NaN);
17319
- const shape = parseShapeArgs2(args);
17320
- if (shape.length === 1) shape.push(shape[0]);
17321
- const n = numel(shape);
17322
- const data = new FloatXArray(n);
17323
- data.fill(NaN);
17324
- return RTV.tensor(data, shape);
17325
- }
17326
- }
17327
- ]);
17378
+ const nanApply = (args) => {
17379
+ if (args.length === 0) return RTV.num(NaN);
17380
+ const shape = parseShapeArgs2(args);
17381
+ if (shape.length === 1) shape.push(shape[0]);
17382
+ const n = numel(shape);
17383
+ const data = new FloatXArray(n);
17384
+ data.fill(NaN);
17385
+ return RTV.tensor(data, shape);
17386
+ };
17387
+ register("NaN", [{ check: arrayConstructorCheck, apply: nanApply }]);
17388
+ register("nan", [{ check: arrayConstructorCheck, apply: nanApply }]);
17328
17389
  register("eye", [
17329
17390
  {
17330
17391
  check: arrayConstructorCheck,
@@ -19600,7 +19661,52 @@ function registerReductionFunctions() {
19600
19661
  if (args.length < 2)
19601
19662
  throw new RuntimeError("ismember requires 2 arguments");
19602
19663
  const v = args[0];
19603
- const bArr = toNumArray(args[1], "ismember");
19664
+ const b = args[1];
19665
+ const isStringLike = (x) => x.kind === "string" || x.kind === "char";
19666
+ const isCellOfStrings = (x) => x.kind === "cell" && x.data.every(
19667
+ (e) => e.kind === "string" || e.kind === "char"
19668
+ );
19669
+ if (isStringLike(v) || isCellOfStrings(v) || isStringLike(b) || isCellOfStrings(b)) {
19670
+ const bStrings = [];
19671
+ if (isStringLike(b)) {
19672
+ bStrings.push(toString(b));
19673
+ } else if (isCellOfStrings(b)) {
19674
+ if (b.kind !== "cell") throw new RuntimeError("unexpected type");
19675
+ for (const e of b.data)
19676
+ bStrings.push(toString(e));
19677
+ } else {
19678
+ throw new RuntimeError("ismember: incompatible argument types");
19679
+ }
19680
+ const bSet = new Set(bStrings);
19681
+ if (isStringLike(v)) {
19682
+ const found = bSet.has(toString(v));
19683
+ const lia = RTV.logical(found);
19684
+ if (nargout > 1) {
19685
+ const idx = found ? bStrings.indexOf(toString(v)) + 1 : 0;
19686
+ return [lia, RTV.num(idx)];
19687
+ }
19688
+ return lia;
19689
+ }
19690
+ if (v.kind === "cell" && isCellOfStrings(v)) {
19691
+ const vData = v.data;
19692
+ const tfData = new FloatXArray(vData.length);
19693
+ const locData = nargout > 1 ? new FloatXArray(vData.length) : void 0;
19694
+ for (let i = 0; i < vData.length; i++) {
19695
+ const s = toString(vData[i]);
19696
+ const found = bSet.has(s);
19697
+ tfData[i] = found ? 1 : 0;
19698
+ if (locData) locData[i] = found ? bStrings.indexOf(s) + 1 : 0;
19699
+ }
19700
+ const t = RTV.tensor(tfData, [...v.shape]);
19701
+ t._isLogical = true;
19702
+ if (nargout > 1) {
19703
+ return [t, RTV.tensor(locData, [...v.shape])];
19704
+ }
19705
+ return t;
19706
+ }
19707
+ throw new RuntimeError("ismember: incompatible argument types");
19708
+ }
19709
+ const bArr = toNumArray(b, "ismember");
19604
19710
  const bMap = /* @__PURE__ */ new Map();
19605
19711
  for (let i = 0; i < bArr.length; i++) {
19606
19712
  if (!bMap.has(bArr[i])) bMap.set(bArr[i], i + 1);
@@ -21541,21 +21647,20 @@ function registerMiscFunctions() {
21541
21647
  return RTV.struct(fields);
21542
21648
  })
21543
21649
  );
21544
- register(
21545
- "fieldnames",
21546
- builtinSingle((args) => {
21547
- if (args.length !== 1)
21548
- throw new RuntimeError("fieldnames requires 1 argument");
21549
- const v = args[0];
21550
- if (v.kind !== "struct" && v.kind !== "class_instance")
21551
- throw new RuntimeError("fieldnames: argument must be a struct");
21552
- const names = [...v.fields.keys()];
21553
- return RTV.cell(
21554
- names.map((n) => RTV.string(n)),
21555
- [names.length, 1]
21556
- );
21557
- })
21558
- );
21650
+ const fieldnamesApply = builtinSingle((args) => {
21651
+ if (args.length !== 1)
21652
+ throw new RuntimeError("fieldnames requires 1 argument");
21653
+ const v = args[0];
21654
+ if (v.kind !== "struct" && v.kind !== "class_instance")
21655
+ throw new RuntimeError("fieldnames: argument must be a struct");
21656
+ const names = [...v.fields.keys()];
21657
+ return RTV.cell(
21658
+ names.map((n) => RTV.string(n)),
21659
+ [names.length, 1]
21660
+ );
21661
+ });
21662
+ register("fieldnames", fieldnamesApply);
21663
+ register("fields", fieldnamesApply);
21559
21664
  register(
21560
21665
  "isfield",
21561
21666
  builtinSingle((args) => {
@@ -22361,7 +22466,6 @@ var dummyHandleFunctions = [
22361
22466
  "gcf",
22362
22467
  "gca",
22363
22468
  "shg",
22364
- "ishold",
22365
22469
  "dir",
22366
22470
  "newplot",
22367
22471
  "caxis",
@@ -22401,7 +22505,7 @@ function registerDummyArrayFunctions() {
22401
22505
  register(name, fn);
22402
22506
  }
22403
22507
  }
22404
- var returnDummyBooleanFunctions = ["ispc", "ismac", "isunix"];
22508
+ var returnDummyBooleanFunctions = ["ispc", "ismac", "isunix", "ishold"];
22405
22509
  function registerDummyBooleanFunctions() {
22406
22510
  const fn = builtinSingle(() => RTV.logical(false), {
22407
22511
  outputType: IType.Logical
@@ -22419,12 +22523,27 @@ function registerDummyCellArrayFunctions() {
22419
22523
  register(name, fn);
22420
22524
  }
22421
22525
  }
22526
+ function registerHandleGetSet() {
22527
+ register(
22528
+ "get",
22529
+ builtinSingle(() => RTV.dummyHandle(), {
22530
+ outputType: IType.DummyHandle
22531
+ })
22532
+ );
22533
+ register(
22534
+ "set",
22535
+ builtinSingle(() => void 0, {
22536
+ outputType: IType.Void
22537
+ })
22538
+ );
22539
+ }
22422
22540
  var registerDummyFunctions = () => {
22423
22541
  registerDummyHandleFunctions();
22424
22542
  registerDummyStringFunctions();
22425
22543
  registerDummyArrayFunctions();
22426
22544
  registerDummyBooleanFunctions();
22427
22545
  registerDummyCellArrayFunctions();
22546
+ registerHandleGetSet();
22428
22547
  };
22429
22548
 
22430
22549
  // src/numbl-core/builtins/index.ts
@@ -25823,9 +25942,6 @@ var CodegenExprs = class extends CodegenBase {
25823
25942
  if (kind.name === "isa") return `$rt.isa(${args[0]}, ${args[1]})`;
25824
25943
  if (kind.name === "figure")
25825
25944
  return `$rt.plot_instr({type: "set_figure_handle", handle: ${args[0] ?? "1"}})`;
25826
- if (kind.name === "plot") {
25827
- return `$rt.plot_call([${args.join(", ")}])`;
25828
- }
25829
25945
  if (kind.name === "hold")
25830
25946
  return `$rt.plot_instr({type: "set_hold", value: ${args[0]}})`;
25831
25947
  if (kind.name === "drawnow") {
@@ -28358,6 +28474,7 @@ var Runtime = class {
28358
28474
  }
28359
28475
  /** Unwrap a RuntimeValue result to native JS types for the fast path */
28360
28476
  unwrapResult(result) {
28477
+ if (result === void 0) return void 0;
28361
28478
  if (Array.isArray(result)) {
28362
28479
  return result.map((r) => {
28363
28480
  if (r.kind === "number") return r.value;
@@ -29595,6 +29712,9 @@ var Runtime = class {
29595
29712
  );
29596
29713
  if (fn) return fn.fn(nargout, ...args);
29597
29714
  }
29715
+ if (name === "plot") {
29716
+ return this.plot_call(args);
29717
+ }
29598
29718
  const builtin = this.builtins[name];
29599
29719
  if (builtin) return builtin(nargout, args);
29600
29720
  throw new RuntimeError(`Undefined function or variable: '${name}'`);
@@ -30212,7 +30332,7 @@ var MIME_TYPES = {
30212
30332
  ".png": "image/png",
30213
30333
  ".json": "application/json"
30214
30334
  };
30215
- async function startPlotServer() {
30335
+ async function startPlotServer(options) {
30216
30336
  const thisDir = fileURLToPath(new URL(".", import.meta.url));
30217
30337
  const distDir = join(thisDir, "..", "dist-plot-viewer");
30218
30338
  if (!existsSync(distDir)) {
@@ -30266,11 +30386,13 @@ Expected directory: ${distDir}`
30266
30386
  res.end("Not found");
30267
30387
  }
30268
30388
  });
30389
+ const listenPort = options?.port ?? 0;
30390
+ const listenHost = options?.host ?? "127.0.0.1";
30269
30391
  await new Promise((resolve2) => {
30270
- server.listen(0, "127.0.0.1", () => resolve2());
30392
+ server.listen(listenPort, listenHost, () => resolve2());
30271
30393
  });
30272
30394
  const port = server.address().port;
30273
- const url = `http://127.0.0.1:${port}/`;
30395
+ const url = `http://${listenHost}:${port}/`;
30274
30396
  console.error(`[numbl] Plot server at ${url}`);
30275
30397
  openBrowser(url);
30276
30398
  let closedResolve;
@@ -30526,9 +30648,11 @@ function jsonReplacer(_key, value) {
30526
30648
  }
30527
30649
  return value;
30528
30650
  }
30529
- async function runRepl() {
30651
+ async function runRepl(plot, plotPort) {
30530
30652
  const workspaceFiles = scanMFiles(process.cwd());
30531
30653
  let variableValues = {};
30654
+ const plotOpts = plotPort !== void 0 ? { port: plotPort, host: "0.0.0.0" } : void 0;
30655
+ const { onDrawnow } = createPlotHandler(!plot, plotOpts);
30532
30656
  const rl = createInterface({
30533
30657
  input: process.stdin,
30534
30658
  output: process.stdout,
@@ -30553,12 +30677,16 @@ async function runRepl() {
30553
30677
  onOutput: (text) => {
30554
30678
  process.stdout.write(text);
30555
30679
  },
30680
+ onDrawnow,
30556
30681
  initialVariableValues: variableValues
30557
30682
  },
30558
30683
  workspaceFiles,
30559
30684
  "repl"
30560
30685
  );
30561
30686
  variableValues = result.variableValues;
30687
+ if (result.plotInstructions.length > 0 && onDrawnow) {
30688
+ onDrawnow(result.plotInstructions);
30689
+ }
30562
30690
  } catch (error) {
30563
30691
  const diags = diagnoseErrors(error, trimmed, "repl", workspaceFiles);
30564
30692
  console.error(formatDiagnostics(diags));
@@ -30750,6 +30878,7 @@ async function main() {
30750
30878
  let noAsync = false;
30751
30879
  let noDce = false;
30752
30880
  let plot = false;
30881
+ let plotPort;
30753
30882
  let addScriptPath = false;
30754
30883
  for (let i = 0; i < args.length; i++) {
30755
30884
  if (args[i] === "--dump-js") {
@@ -30781,6 +30910,18 @@ async function main() {
30781
30910
  noDce = true;
30782
30911
  } else if (args[i] === "--plot") {
30783
30912
  plot = true;
30913
+ } else if (args[i] === "--plot-port") {
30914
+ i++;
30915
+ if (i >= args.length) {
30916
+ console.error("Error: --plot-port requires a port number");
30917
+ process.exit(1);
30918
+ }
30919
+ plotPort = parseInt(args[i], 10);
30920
+ if (isNaN(plotPort)) {
30921
+ console.error("Error: --plot-port requires a valid port number");
30922
+ process.exit(1);
30923
+ }
30924
+ plot = true;
30784
30925
  } else if (args[i] === "--add-script-path") {
30785
30926
  addScriptPath = true;
30786
30927
  } else if (args[i] === "--path") {
@@ -30810,7 +30951,7 @@ async function main() {
30810
30951
  return;
30811
30952
  }
30812
30953
  if (evalCode === null && positional.length === 0) {
30813
- await runRepl();
30954
+ await runRepl(plot, plotPort);
30814
30955
  return;
30815
30956
  }
30816
30957
  let code = "";
@@ -30945,14 +31086,19 @@ async function main() {
30945
31086
  workspaceFiles,
30946
31087
  mainFileName
30947
31088
  );
31089
+ const syncPlotOpts = plotPort !== void 0 ? { port: plotPort, host: "0.0.0.0" } : void 0;
30948
31090
  if (result.plotInstructions.length > 0 && plot) {
30949
- const plotServer = await startPlotServer();
31091
+ const plotServer = await startPlotServer(syncPlotOpts);
30950
31092
  plotServer.sendInstructions(result.plotInstructions);
30951
31093
  plotServer.scriptDone();
30952
31094
  await plotServer.closed;
30953
31095
  }
30954
31096
  } else {
30955
- const { onDrawnow, flushAndWait } = createPlotHandler(!plot);
31097
+ const asyncPlotOpts = plotPort !== void 0 ? { port: plotPort, host: "0.0.0.0" } : void 0;
31098
+ const { onDrawnow, flushAndWait } = createPlotHandler(
31099
+ !plot,
31100
+ asyncPlotOpts
31101
+ );
30956
31102
  const result = await executeCode(
30957
31103
  code,
30958
31104
  {
@@ -30975,7 +31121,7 @@ async function main() {
30975
31121
  process.exit(1);
30976
31122
  }
30977
31123
  }
30978
- function createPlotHandler(disabled) {
31124
+ function createPlotHandler(disabled, plotOpts) {
30979
31125
  if (disabled) {
30980
31126
  return {
30981
31127
  onDrawnow: void 0,
@@ -30992,7 +31138,7 @@ function createPlotHandler(disabled) {
30992
31138
  } else {
30993
31139
  pendingBatches.push(instructions);
30994
31140
  if (!serverStarting) {
30995
- serverStarting = startPlotServer().then((ps) => {
31141
+ serverStarting = startPlotServer(plotOpts).then((ps) => {
30996
31142
  plotServer = ps;
30997
31143
  for (const batch of pendingBatches) {
30998
31144
  ps.sendInstructions(batch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numbl",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Run .m source files in the browser and on the command line by compiling to JavaScript",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",