nomen-lang 0.0.9 → 0.0.11

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/dist/index.mjs CHANGED
@@ -3742,6 +3742,9 @@ var ForLoopNode = class extends BaseNode {
3742
3742
  index;
3743
3743
  update;
3744
3744
  statements;
3745
+ /** `for ref x of arr` — the user wants mutable element access. For arrays
3746
+ * this desugars to copy-out / mutate / copy-back via .at()/.set(). */
3747
+ item_is_ref;
3745
3748
  constructor(start, item, list, statements, update) {
3746
3749
  super("for", start);
3747
3750
  this.item = item;
@@ -5094,6 +5097,7 @@ function build_break_node$1(status) {
5094
5097
  const loop = status.loop_labels?.[status.loop_labels.length - 1];
5095
5098
  if (loop) {
5096
5099
  emit_cleanup_to_loop_depth(status);
5100
+ status.loop_writebacks?.[status.loop_writebacks.length - 1]?.();
5097
5101
  status.code += `b ${loop.end}\n`;
5098
5102
  }
5099
5103
  }
@@ -5172,6 +5176,7 @@ function build_continue_node$1(status) {
5172
5176
  const loop = status.loop_labels?.[status.loop_labels.length - 1];
5173
5177
  if (loop) {
5174
5178
  emit_cleanup_to_loop_depth(status);
5179
+ status.loop_writebacks?.[status.loop_writebacks.length - 1]?.();
5175
5180
  status.code += `b ${loop.start}\n`;
5176
5181
  }
5177
5182
  }
@@ -6604,6 +6609,8 @@ function build_for_loop_node$1(node, status) {
6604
6609
  end: end_label,
6605
6610
  cleanup_depth
6606
6611
  });
6612
+ if (!status.loop_writebacks) status.loop_writebacks = [];
6613
+ status.loop_writebacks.push(void 0);
6607
6614
  if (status.function_return_label) {
6608
6615
  const item_offset = allocate_stack_space(status, 8);
6609
6616
  status.stack_offsets.set(item_name, item_offset);
@@ -6817,7 +6824,49 @@ function build_for_loop_node$1(node, status) {
6817
6824
  else status.code += `ldr x0, [x0]\n`;
6818
6825
  emit_var_store(status, "x0", item_name, element_size);
6819
6826
  }
6827
+ if (node.item_is_ref) {
6828
+ const wb_struct_type = struct_type;
6829
+ const wb_element_size = element_size;
6830
+ const wb_item_name = item_name;
6831
+ const wb_idx_name = idx_name;
6832
+ const wb_list_name = list_name;
6833
+ const wb_list_is_pointer = list_is_pointer;
6834
+ const wb_shift = shift;
6835
+ status.loop_writebacks[status.loop_writebacks.length - 1] = () => {
6836
+ if (wb_list_is_pointer) {
6837
+ emit_var_load(status, "x9", wb_list_name, 8);
6838
+ if (status.heap_array_vars?.has(wb_list_name)) status.code += `add x9, x9, #8\n`;
6839
+ } else emit_var_address(status, "x9", wb_list_name);
6840
+ emit_var_load(status, "x10", wb_idx_name, 8);
6841
+ if (Number.isInteger(wb_shift) && wb_shift > 0) status.code += `add x9, x9, x10, lsl #${wb_shift}\n`;
6842
+ else {
6843
+ status.code += `mov x11, #${wb_element_size}\n`;
6844
+ status.code += `mul x10, x10, x11\n`;
6845
+ status.code += `add x9, x9, x10\n`;
6846
+ }
6847
+ if (wb_struct_type) {
6848
+ const item_offset = status.stack_offsets.get(wb_item_name);
6849
+ if (item_offset !== void 0) {
6850
+ const words = Math.ceil(wb_element_size / 8);
6851
+ for (let w = 0; w < words; w++) {
6852
+ status.code += `ldr x10, [x29, #${item_offset + w * 8}]\n`;
6853
+ status.code += `str x10, [x9, #${w * 8}]\n`;
6854
+ }
6855
+ }
6856
+ } else if (wb_element_size === 1) {
6857
+ emit_var_load(status, "x10", wb_item_name, wb_element_size);
6858
+ status.code += `strb w10, [x9]\n`;
6859
+ } else if (wb_element_size === 4) {
6860
+ emit_var_load(status, "x10", wb_item_name, wb_element_size);
6861
+ status.code += `str w10, [x9]\n`;
6862
+ } else {
6863
+ emit_var_load(status, "x10", wb_item_name, wb_element_size);
6864
+ status.code += `str x10, [x9]\n`;
6865
+ }
6866
+ };
6867
+ }
6820
6868
  build_block_node$1(node, status);
6869
+ status.loop_writebacks[status.loop_writebacks.length - 1]?.();
6821
6870
  if (node.update) {
6822
6871
  status.code += `${continue_label}:\n`;
6823
6872
  build_node$1(node.update, status);
@@ -6835,6 +6884,7 @@ function build_for_loop_node$1(node, status) {
6835
6884
  else status.register_allocations = void 0;
6836
6885
  status.buffer_data_cache = saved_buffer_cache;
6837
6886
  status.loop_labels.pop();
6887
+ status.loop_writebacks?.pop();
6838
6888
  status.scoped_declarations = old_scoped_declarations;
6839
6889
  }
6840
6890
  function is_enumerable_type$1(node, status) {
@@ -14064,6 +14114,7 @@ function build_async_block_node(node, status) {
14064
14114
  //#region ../src/build_c/build_break_node.ts
14065
14115
  function build_break_node(node, status) {
14066
14116
  reclaim_to_loop_body(status);
14117
+ status.loop_writebacks?.[status.loop_writebacks.length - 1]?.();
14067
14118
  status.code += `break;\n`;
14068
14119
  }
14069
14120
  //#endregion
@@ -14088,6 +14139,7 @@ function build_cast_node(node, status) {
14088
14139
  //#region ../src/build_c/build_continue_node.ts
14089
14140
  function build_continue_node(node, status) {
14090
14141
  reclaim_to_loop_body(status);
14142
+ status.loop_writebacks?.[status.loop_writebacks.length - 1]?.();
14091
14143
  status.code += `continue;\n`;
14092
14144
  }
14093
14145
  //#endregion
@@ -14373,6 +14425,7 @@ function build_for_loop_node(node, status) {
14373
14425
  const old_deferred_frees = status.deferred_frees;
14374
14426
  status.deferred_frees = [];
14375
14427
  push_c_loop_frame(status);
14428
+ let ref_writeback;
14376
14429
  if (node.item && node.list) if (node.list.node_type == "range") {
14377
14430
  status.code += "{\n";
14378
14431
  status.code += `${c_type("int")} `;
@@ -14439,14 +14492,27 @@ function build_for_loop_node(node, status) {
14439
14492
  build_node(node.list, status);
14440
14493
  status.code += `[${idx_var}];\n`;
14441
14494
  }
14495
+ if (node.item_is_ref) {
14496
+ let wb_target;
14497
+ if (is_heap) wb_target = `((${elem_is_class ? `struct ${element_type} **` : `${elem_struct ? `struct ${element_type}` : c_type(element_type)} *`})((char *)${list_name} + sizeof(struct Array_${element_type})))[${idx_var}]`;
14498
+ else wb_target = `${list_name}[${idx_var}]`;
14499
+ const wb_code = `${wb_target} = ${node.item.value};\n`;
14500
+ ref_writeback = () => {
14501
+ status.code += wb_code;
14502
+ };
14503
+ }
14442
14504
  }
14505
+ if (!status.loop_writebacks) status.loop_writebacks = [];
14506
+ status.loop_writebacks.push(ref_writeback);
14443
14507
  build_block_node(node, status);
14444
14508
  if (node.update) {
14445
14509
  build_node(node.update, status);
14446
14510
  status.code += ";\n";
14447
14511
  }
14512
+ if (ref_writeback) ref_writeback();
14448
14513
  build_auto_free(status);
14449
14514
  status.code += `}\n`;
14515
+ status.loop_writebacks.pop();
14450
14516
  if (node.list && (node.list.node_type == "range" || is_enumerable_type(node.list, status))) status.code += `}\n`;
14451
14517
  pop_c_loop_frame(status);
14452
14518
  leave_c_scope(status);
@@ -15410,8 +15476,7 @@ const CALL_KEYWORDS = /* @__PURE__ */ new Set([
15410
15476
  "trait",
15411
15477
  "enum",
15412
15478
  "bitset",
15413
- "cast",
15414
- "view"
15479
+ "cast"
15415
15480
  ]);
15416
15481
  const NO_SPACE_BEFORE = /* @__PURE__ */ new Set([
15417
15482
  ",",
@@ -16021,9 +16086,10 @@ const PAIRS = {
16021
16086
  * formatted source doesn't tokenize to exactly the same stream, the original is
16022
16087
  * returned untouched. Nomen's tokenizer folds a sign into the number that
16023
16088
  * follows it (`a -1` is not `a - 1`), so a spacing slip really can change the
16024
- * meaning of a program — never let one through.
16089
+ * meaning of a program — never let one through. Pass `force` to apply the
16090
+ * layout anyway (e.g. for an explicit "force format" command).
16025
16091
  */
16026
- function format_source(source, options) {
16092
+ function format_source(source, options, force = false) {
16027
16093
  const config = {
16028
16094
  ...default_format_options,
16029
16095
  ...options
@@ -16034,7 +16100,7 @@ function format_source(source, options) {
16034
16100
  strip_redundant_types: false
16035
16101
  });
16036
16102
  const difference = token_difference(source, layout);
16037
- if (difference) return {
16103
+ if (difference && !force) return {
16038
16104
  code: source,
16039
16105
  changed: false,
16040
16106
  unsafe: difference
@@ -16308,14 +16374,22 @@ function describe(token) {
16308
16374
  }
16309
16375
  //#endregion
16310
16376
  //#region ../src/join.ts
16311
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
16312
- const DEFAULT_LIB_PATH = path.resolve(__dirname, "../../core/src");
16377
+ const DEFAULT_LIB_REL = "../../core/src";
16378
+ let default_lib_path;
16379
+ function fallback_lib_path() {
16380
+ if (default_lib_path === void 0) try {
16381
+ default_lib_path = path.resolve(path.dirname(fileURLToPath(import.meta.url)), DEFAULT_LIB_REL);
16382
+ } catch {
16383
+ default_lib_path = "";
16384
+ }
16385
+ return default_lib_path;
16386
+ }
16313
16387
  let test_src_dir;
16314
16388
  function join$1(entry_file_path, lib_path, options) {
16315
16389
  const folder_path = path.dirname(entry_file_path);
16316
16390
  const file_path = path.basename(entry_file_path);
16317
16391
  const inputs = /* @__PURE__ */ new Map();
16318
- const resolved_lib_path = lib_path ? path.resolve(lib_path, "src") : DEFAULT_LIB_PATH;
16392
+ const resolved_lib_path = lib_path ? path.resolve(lib_path, "src") : fallback_lib_path();
16319
16393
  const for_test = options?.for_test ?? file_path.endsWith(".test.nm");
16320
16394
  const prev_test_src_dir = test_src_dir;
16321
16395
  test_src_dir = for_test ? resolve_src_module(folder_path) : void 0;
@@ -16759,14 +16833,20 @@ function struct_owns_heap(s, status, visited) {
16759
16833
  //#endregion
16760
16834
  //#region ../src/check/utils/type_name.ts
16761
16835
  function type_name(type) {
16762
- if (type.tuple_types?.length && (type.name === "tuple" || type.name.startsWith("_Tuple_"))) return `[${type.tuple_types.map((t) => type_name(t)).join(", ")}]${type.is_nullable ? "?" : ""}`;
16836
+ const prefix = type_modifier(type);
16837
+ if (type.tuple_types?.length && (type.name === "tuple" || type.name.startsWith("_Tuple_"))) return `${prefix}[${type.tuple_types.map((t) => type_name(t)).join(", ")}]${type.is_nullable ? "?" : ""}`;
16763
16838
  if (type.is_array) return `Array<${type_name_without_array(type)}>${type.is_nullable ? "?" : ""}`;
16764
16839
  const args = type.type_args?.length ? `<${type.type_args.map((t) => type_name(t)).join(", ")}>` : "";
16765
- return `${type.is_ref ? "ref " : ""}${type.name}${args}${type.is_nullable ? "?" : ""}`;
16840
+ return `${prefix}${type.name}${args}${type.is_nullable ? "?" : ""}`;
16841
+ }
16842
+ function type_modifier(type) {
16843
+ if (type.is_view) return "view ";
16844
+ if (type.is_ref) return "ref ";
16845
+ return "";
16766
16846
  }
16767
16847
  function type_name_without_array(type) {
16768
16848
  const args = type.type_args?.length ? `<${type.type_args.map((t) => type_name(t)).join(", ")}>` : "";
16769
- return `${type.is_ref ? "ref " : ""}${type.name}${args}`;
16849
+ return `${type_modifier(type)}${type.name}${args}`;
16770
16850
  }
16771
16851
  //#endregion
16772
16852
  //#region ../src/check/utils/check_type_and_value_match.ts
@@ -17685,6 +17765,7 @@ function evaluate_operation(op, status) {
17685
17765
  if (op.op === "<" || op.op === "<=" || op.op === ">" || op.op === ">=") {
17686
17766
  const num = evaluate_numeric_comparison(op, status);
17687
17767
  if (num !== void 0) return num;
17768
+ if (op.op === "<=" && is_zero(op.left_value, status) && is_nonnegative_access(op.right_value) || op.op === ">=" && is_zero(op.right_value, status) && is_nonnegative_access(op.left_value)) return true;
17688
17769
  let left_var;
17689
17770
  let left_offset = 0;
17690
17771
  if (op.left_value.node_type === "value") left_var = op.left_value.value;
@@ -17981,6 +18062,25 @@ function evaluate_numeric_or_bool(node, status) {
17981
18062
  return evaluate_const_condition(op_node, status);
17982
18063
  }
17983
18064
  }
18065
+ const NON_NEGATIVE_FIELDS = /* @__PURE__ */ new Set([
18066
+ "length",
18067
+ "cap",
18068
+ "count",
18069
+ "size"
18070
+ ]);
18071
+ /** Does `node` read a container's length/capacity, which is always >= 0? */
18072
+ function is_nonnegative_access(node) {
18073
+ if (node.node_type !== "access") return false;
18074
+ const access = node;
18075
+ if (access.access.node_type === "access_field") return NON_NEGATIVE_FIELDS.has(access.access.name);
18076
+ if (access.access.node_type === "access_func") return NON_NEGATIVE_FIELDS.has(access.access.name);
18077
+ return false;
18078
+ }
18079
+ /** Does `node` evaluate to the compile-time integer 0 (literal or const)? */
18080
+ function is_zero(node, status) {
18081
+ const value = evaluate_numeric_or_bool(node, status);
18082
+ return typeof value === "number" && value === 0;
18083
+ }
17984
18084
  //#endregion
17985
18085
  //#region ../src/check/utils/is_visible.ts
17986
18086
  /**
@@ -18352,10 +18452,12 @@ function check_function_call(node, status, func, target_type, self_value, self_p
18352
18452
  }
18353
18453
  const satisfied = evaluate_const_condition(func_param.constraint, status);
18354
18454
  status.values.length = saved_values_length;
18355
- if (satisfied === false) add_error(status, `Parameter constraint not satisfied: ${func_param.name}`, param.start);
18455
+ const constraint_source = expression_to_source(func_param.constraint);
18456
+ const suffix = constraint_source ? `\n ${constraint_source}` : "";
18457
+ if (satisfied === false) add_error(status, `Parameter constraint not satisfied: ${func_param.name}${suffix}`, param.start);
18356
18458
  else if (satisfied === void 0) {
18357
- if (!is_inside_core_method(status)) add_error(status, `Parameter constraint cannot be verified: ${func_param.name}`, param.start);
18358
- } else if (satisfied === "unsafe") add_error(status, `Parameter constraint not satisfied: ${func_param.name}`, param.start);
18459
+ if (!is_inside_core_method(status)) add_error(status, `Parameter constraint cannot be verified: ${func_param.name}${suffix}`, param.start);
18460
+ } else if (satisfied === "unsafe") add_error(status, `Parameter constraint not satisfied: ${func_param.name}${suffix}`, param.start);
18359
18461
  }
18360
18462
  if (param.node_type !== "value" && !has_ref_keyword && !node.swap_params?.has(i) && !(i === node.variadic_param_index && param.node_type === "array")) {
18361
18463
  const declaration_name = `_param_${status.var_name_counter.value++}`;
@@ -18444,6 +18546,29 @@ function find_lhs_var_name(status) {
18444
18546
  if (node.node_type === "func") return void 0;
18445
18547
  }
18446
18548
  }
18549
+ /** Render a constraint expression AST back to a source-like string. */
18550
+ function expression_to_source(node) {
18551
+ if (!node) return "";
18552
+ if (node.node_type === "value") return node.value;
18553
+ if (node.node_type === "access") {
18554
+ const access = node;
18555
+ const target = expression_to_source(access.target);
18556
+ if (access.access.node_type === "access_field") {
18557
+ const name = access.access.name;
18558
+ return target ? `${target}.${name}` : name;
18559
+ }
18560
+ if (access.access.node_type === "access_func") {
18561
+ const name = access.access.name;
18562
+ return target ? `${target}.${name}()` : `${name}()`;
18563
+ }
18564
+ return target;
18565
+ }
18566
+ if (node.node_type === "op") {
18567
+ const op = node;
18568
+ return `${expression_to_source(op.left_value)} ${op.op} ${expression_to_source(op.right_value)}`;
18569
+ }
18570
+ return "";
18571
+ }
18447
18572
  //#endregion
18448
18573
  //#region ../src/nodes/clone_node.ts
18449
18574
  function clone_type$2(type) {
@@ -19024,7 +19149,12 @@ function materialize_tuple_type(type, status) {
19024
19149
  //#endregion
19025
19150
  //#region ../src/check/check_declaration_node.ts
19026
19151
  function check_declaration_node(decl, status) {
19152
+ const is_view_keyword = decl.declaration === "view";
19153
+ const declaration = decl.declaration === "view" ? "const" : decl.declaration;
19154
+ decl.declaration = declaration;
19155
+ const declared_type_is_view = !!decl.type.is_view;
19027
19156
  if (decl.func_params) {
19157
+ if (is_view_keyword) add_error(status, `'view' cannot declare a function-typed binding`, decl.start);
19028
19158
  if (decl.func_return_type) {
19029
19159
  check_type_exists(decl.func_return_type, status, -1);
19030
19160
  if (decl.func_return_type.name === "tuple" && decl.func_return_type.tuple_types?.length) decl.func_return_type = materialize_tuple_type(decl.func_return_type, status);
@@ -19062,13 +19192,13 @@ function check_declaration_node(decl, status) {
19062
19192
  status.stack.pop();
19063
19193
  }
19064
19194
  status.values.push({
19065
- declaration: decl.declaration,
19195
+ declaration,
19066
19196
  name: decl.name,
19067
19197
  type: decl.func_return_type || decl.type,
19068
19198
  is_set: !!decl.value,
19069
19199
  start: decl.start,
19070
19200
  is_null: decl.value?.node_type === "value" && decl.value.value === "null",
19071
- const_value: decl.declaration === "const" ? extract_const_value(decl.value) : void 0,
19201
+ const_value: declaration === "const" ? extract_const_value(decl.value) : void 0,
19072
19202
  constraint: decl.constraint,
19073
19203
  func_params: decl.func_params?.map((p) => ({
19074
19204
  name: p.name,
@@ -19095,6 +19225,8 @@ function check_declaration_node(decl, status) {
19095
19225
  }
19096
19226
  status.stack.pop();
19097
19227
  }
19228
+ if (is_view_keyword && decl.value && !decl.type.is_view) add_error(status, `'view' binding requires a view value (e.g. a .slice() result), got ${decl.type.name}`, decl.value.start);
19229
+ if (!is_view_keyword && !declared_type_is_view && decl.type.is_view && decl.value) add_error(status, `binding a view requires the 'view' keyword (e.g. 'view name = ...' or 'var view name = ...')`, decl.value.start);
19098
19230
  if (decl.type.name === "string" && decl.value && decl.value.node_type === "value" && decl.value.value.startsWith("\"") && decl.value.value.endsWith("\"")) {
19099
19231
  const len = decl.value.value.length - 2;
19100
19232
  decl.type.length = new ValueNode(decl.value.start, len.toString(), new Type("int"));
@@ -19119,13 +19251,13 @@ function check_declaration_node(decl, status) {
19119
19251
  check_constraint(decl, status);
19120
19252
  const class_alias_src = decl.value?.node_type === "value" && !decl.value.is_moved && decl.value.value !== "null" && decl.type.name && is_class_type$1(decl.type.name, status) ? decl.value.value : void 0;
19121
19253
  status.values.push({
19122
- declaration: decl.declaration,
19254
+ declaration,
19123
19255
  name: decl.name,
19124
19256
  type: decl.type,
19125
19257
  is_set: !!decl.value,
19126
19258
  start: decl.start,
19127
19259
  is_null: decl.value?.node_type === "value" && decl.value.value === "null",
19128
- const_value: decl.declaration === "const" ? extract_const_value(decl.value) : void 0,
19260
+ const_value: declaration === "const" ? extract_const_value(decl.value) : void 0,
19129
19261
  constraint: decl.constraint,
19130
19262
  decl_depth: status.scope_depth,
19131
19263
  borrow_depth: decl.value ? borrow_depth_of(decl.value, status) : void 0,
@@ -19249,8 +19381,8 @@ function check_struct_node(struct, status) {
19249
19381
  const values_length_before_fields = status.values.length;
19250
19382
  for (let decl of struct.fields) {
19251
19383
  decl.scope = struct;
19252
- if (decl.type.is_ref || decl.type.is_view) {
19253
- add_error(status, `${struct.is_class ? "class" : "struct"} fields cannot be '${decl.type.is_view ? "view" : "ref"}'`, decl.start);
19384
+ if (decl.type.is_ref || decl.type.is_view || decl.declaration === "view") {
19385
+ add_error(status, `${struct.is_class ? "class" : "struct"} fields cannot be '${decl.type.is_view || decl.declaration === "view" ? "view" : "ref"}'`, decl.start);
19254
19386
  continue;
19255
19387
  }
19256
19388
  if (!struct.is_class && is_class_type$1(decl.type.name, status)) add_error(status, `struct fields cannot be class types, use a class instead`, decl.start);
@@ -21715,6 +21847,7 @@ function check_for_loop_node(for_loop, status) {
21715
21847
  const has_at = list_type.name !== void 0 && !!status.structs.find((s) => s.name === "Array_" + list_type.name && s.functions.some((f) => f.name === "at"));
21716
21848
  if (list_type.is_array && !enumerable && has_at) {
21717
21849
  desugar_array_for_loop(for_loop, list_type);
21850
+ for_loop.item_is_ref = false;
21718
21851
  check_for_loop_node(for_loop, status);
21719
21852
  return;
21720
21853
  }
@@ -21725,7 +21858,13 @@ function check_for_loop_node(for_loop, status) {
21725
21858
  const list_type = type_from_value_node(for_loop.list, for_status);
21726
21859
  const is_enumerable = list_type.name ? has_trait(list_type.name, "Enumerable", for_status) : false;
21727
21860
  if (!list_type.is_array && !is_enumerable && list_type.name) add_error(for_status, `For loop list must be an array or Enumerable, not ${list_type.name}`, for_loop.list.start);
21861
+ if (for_loop.item_is_ref && list_type.is_array && for_loop.list.node_type === "value") {
21862
+ const list_name = for_loop.list.value;
21863
+ const list_value = for_status.values.find((v) => v.name === list_name);
21864
+ if (list_value && list_value.declaration !== "var") add_error(for_status, `'ref' iteration requires a 'var' array, but '${list_name}' is const`, for_loop.list.start);
21865
+ }
21728
21866
  if (for_loop.item) {
21867
+ if (for_loop.item_is_ref && (for_loop.list instanceof RangeNode || is_enumerable)) add_error(for_status, `'ref' is only valid for array element iteration (for ref x of arr), not ranges or Enumerable types`, for_loop.item.start);
21729
21868
  if (is_enumerable) for_loop.item.type = new Type("int", true);
21730
21869
  else for_loop.item.type = new Type(list_type.name);
21731
21870
  let range_lower;
@@ -21746,7 +21885,7 @@ function check_for_loop_node(for_loop, status) {
21746
21885
  if (upper_bound_expr) upper_bound_expr = upper_bound_expr + ".length";
21747
21886
  }
21748
21887
  for_status.values.push({
21749
- declaration: "var",
21888
+ declaration: for_loop.item_is_ref ? "var" : "const",
21750
21889
  name: for_loop.item.value,
21751
21890
  type: for_loop.item.type,
21752
21891
  is_set: true,
@@ -21761,19 +21900,56 @@ function check_for_loop_node(for_loop, status) {
21761
21900
  if (for_loop.update) check_node(for_loop.update, for_status);
21762
21901
  persist_invalidated(status, for_status);
21763
21902
  }
21764
- /** Rewrite `for x of arr` in place into `for __idx of 0..arr.length { var x = arr.at(__idx) }`. */
21903
+ /**
21904
+ * Rewrite `for x of arr` in place into `for __idx of 0..arr.length { x = arr.at(__idx) }`.
21905
+ *
21906
+ * By default `x` is const (read-only). For `for ref x of arr` the desugaring is
21907
+ * copy-out / mutate / copy-back: `var x = arr.at(__idx)` is prepended and
21908
+ * `arr.set(__idx, x)` is appended, with the write-back duplicated before every
21909
+ * break/continue so mutations persist on all exit paths. (The write-back is
21910
+ * skipped on `return`, matching Rust's copy semantics.)
21911
+ */
21765
21912
  function desugar_array_for_loop(for_loop, array_type) {
21766
21913
  const list = for_loop.list;
21767
21914
  const start = list.start;
21768
21915
  const original_item = for_loop.item;
21769
21916
  const idx_name = `__for_idx_${original_item.value}`;
21917
+ const is_ref = !!for_loop.item_is_ref;
21770
21918
  for_loop.item = new ValueNode(original_item.start, idx_name);
21771
21919
  for_loop.list = new RangeNode(start, new ValueNode(start, "0"), array_type.length ? clone_node(array_type.length) : new AccessNode(start, clone_node(list), new AccessFieldNode(start, "length")));
21772
21920
  const at_call = new AccessFunctionCallNode(start, "at", new Type(""), [new ValueNode(start, idx_name)]);
21773
21921
  const at_access = new AccessNode(start, clone_node(list), at_call);
21774
- const decl = new DeclarationNode(original_item.start, "private", "var", original_item.value, new Type(array_type.name), at_access);
21922
+ const decl = new DeclarationNode(original_item.start, "private", is_ref ? "var" : "const", original_item.value, new Type(array_type.name), at_access);
21775
21923
  decl.is_loop_iterator = true;
21776
21924
  for_loop.statements.unshift(decl);
21925
+ if (is_ref) {
21926
+ const make_writeback = () => {
21927
+ const set_call = new AccessFunctionCallNode(start, "set", new Type(""), [new ValueNode(start, idx_name), new ValueNode(start, original_item.value)]);
21928
+ return new AccessNode(start, clone_node(list), set_call);
21929
+ };
21930
+ insert_writebacks(for_loop.statements, make_writeback);
21931
+ for_loop.statements.push(make_writeback());
21932
+ }
21933
+ }
21934
+ /**
21935
+ * Walk a statement array and insert a write-back node before every break /
21936
+ * continue that belongs to the current loop. Nested for/while loops and nested
21937
+ * function bodies are not descended into (their exits belong to them).
21938
+ */
21939
+ function insert_writebacks(statements, make_writeback) {
21940
+ for (let i = statements.length - 1; i >= 0; i--) {
21941
+ const stmt = statements[i];
21942
+ if (stmt.node_type === "break" || stmt.node_type === "continue") {
21943
+ statements.splice(i, 0, make_writeback());
21944
+ continue;
21945
+ }
21946
+ if (stmt.node_type === "for" || stmt.node_type === "while" || stmt.node_type === "func") continue;
21947
+ for (const key of Object.keys(stmt)) {
21948
+ if (key === "parent" || key === "scope") continue;
21949
+ const val = stmt[key];
21950
+ if (Array.isArray(val)) insert_writebacks(val, make_writeback);
21951
+ }
21952
+ }
21777
21953
  }
21778
21954
  function evaluate_range_bound_value(node, status) {
21779
21955
  const val = evaluate_numeric_or_bool(node, status);
@@ -22688,6 +22864,10 @@ function warn_var_not_changed(decl, locals, status) {
22688
22864
  if (is_discard(decl.name)) return;
22689
22865
  if (locals.assigned.has(decl.name)) return;
22690
22866
  if (status.mutated_local_names?.has(decl.name)) return;
22867
+ if (decl.type?.is_view) {
22868
+ add_warning(status, `View '${decl.name}' is never re-pointed, consider dropping 'var' (use 'view ${decl.name} = ...')`, decl.name_start ?? decl.start);
22869
+ return;
22870
+ }
22691
22871
  add_warning(status, `Variable '${decl.name}' is never changed, consider using const`, decl.name_start ?? decl.start);
22692
22872
  }
22693
22873
  /** Names that count as a "use": call targets, method names, and value reads. */
@@ -23040,12 +23220,23 @@ function parse_declaration(visibility, declaration, status) {
23040
23220
  decl.type_start = get_index(status);
23041
23221
  decl.type = parse_type(status);
23042
23222
  if (peek_current(status) === "=" || status.i >= status.tokens.length) {
23043
- status.i = saved_i;
23044
- status.errors.length = saved_errors_length;
23045
- decl.type = new Type("");
23046
- decl.type_start = void 0;
23047
- decl.name_start = get_index(status);
23048
- decl.name = consume(status);
23223
+ const modifier = status.tokens[saved_i]?.value;
23224
+ if ((modifier === "view" || modifier === "ref") && decl.type.name) {
23225
+ decl.name = decl.type.name;
23226
+ decl.name_start = saved_i + 1;
23227
+ const inferred = new Type("");
23228
+ if (modifier === "view") inferred.is_view = true;
23229
+ else inferred.is_ref = true;
23230
+ decl.type = inferred;
23231
+ decl.type_start = void 0;
23232
+ } else {
23233
+ status.i = saved_i;
23234
+ status.errors.length = saved_errors_length;
23235
+ decl.type = new Type("");
23236
+ decl.type_start = void 0;
23237
+ decl.name_start = get_index(status);
23238
+ decl.name = consume(status);
23239
+ }
23049
23240
  } else {
23050
23241
  decl.name_start = get_index(status);
23051
23242
  decl.name = consume(status);
@@ -24270,13 +24461,16 @@ function parse_extend(visibility, status) {
24270
24461
  function parse_for_loop(status) {
24271
24462
  const for_start = get_index(status);
24272
24463
  accept("for", status);
24273
- const item = new ValueNode(get_index(status), consume(status));
24464
+ const start = get_index(status);
24465
+ const item_is_ref = accept("ref", status);
24466
+ const item = new ValueNode(start, consume(status));
24274
24467
  if (expect("of", status)) {
24275
24468
  const list = parse_expression(status);
24276
24469
  let update;
24277
24470
  if (accept(";", status)) update = parse_expression(status);
24278
24471
  if (expect("{", status)) {
24279
24472
  const for_loop = new ForLoopNode(for_start, item, list, void 0, update);
24473
+ for_loop.item_is_ref = item_is_ref;
24280
24474
  status.stack.push(for_loop);
24281
24475
  parse_statement(status);
24282
24476
  expect("}", status);
@@ -24374,6 +24568,7 @@ function parse_visibility(visibility, status) {
24374
24568
  case "const":
24375
24569
  case "var":
24376
24570
  case "mov":
24571
+ case "view":
24377
24572
  if (visibility === "private" && status.stack.at(-1)?.node_type === "trait") {
24378
24573
  add_error(status, `Trait fields cannot be private`, get_index(status));
24379
24574
  consume(status);
@@ -24465,6 +24660,7 @@ function parse_statement(status) {
24465
24660
  case "const":
24466
24661
  case "var":
24467
24662
  case "mov":
24663
+ case "view":
24468
24664
  parse_declaration(default_visibility(status), value, status);
24469
24665
  break;
24470
24666
  case "struct":
@@ -25223,7 +25419,7 @@ const MAIN_NM = `import System
25223
25419
  pub func add = (int a, int b) => a + b
25224
25420
 
25225
25421
  pub func main = (Init init) {
25226
- Console.write("Hello world, 2 + 2 is \\{add(a + b)}!\\n")
25422
+ Console.write("Hello world, 2 + 2 is \\{add(2, 2)}!\\n")
25227
25423
  }
25228
25424
  `;
25229
25425
  const TEST_NM = `import System
@@ -25307,6 +25503,30 @@ function run_init(name) {
25307
25503
  console.log(`\nNext: cd ${name} && nomen run`);
25308
25504
  }
25309
25505
  //#endregion
25506
+ //#region src/paths.ts
25507
+ /**
25508
+ * The project root for `input_path`: the nearest ancestor folder (or the input
25509
+ * folder itself) containing a `package.jsonc`. Falls back to the input's own
25510
+ * folder for standalone files, so builds stay next to their source.
25511
+ */
25512
+ function project_root_for(input_path) {
25513
+ let dir = fs.lstatSync(input_path).isDirectory() ? input_path : path.dirname(input_path);
25514
+ for (let i = 0; i < 20; i++) {
25515
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
25516
+ const parent = path.dirname(dir);
25517
+ if (parent === dir) break;
25518
+ dir = parent;
25519
+ }
25520
+ return path.dirname(input_path);
25521
+ }
25522
+ /**
25523
+ * The folder that receives compiler output for `input_path`: the project's
25524
+ * `build/`, or `build/test` for `*.test.nm` files.
25525
+ */
25526
+ function build_dir_for(input_path, is_test) {
25527
+ return is_test ? path.join(project_root_for(input_path), "build", "test") : path.join(project_root_for(input_path), "build");
25528
+ }
25529
+ //#endregion
25310
25530
  //#region src/test.ts
25311
25531
  const RECORD_PREFIX = "\\nomen|";
25312
25532
  /** Recursively collect `*.test.nm` files under `root`, skipping build output. */
@@ -25487,7 +25707,7 @@ function run_test_file(entry_path, lib_path, arch) {
25487
25707
  result.ms = performance.now() - start;
25488
25708
  return result;
25489
25709
  }
25490
- const buildDir = path.join(path.dirname(resolved), "build");
25710
+ const buildDir = build_dir_for(resolved, true);
25491
25711
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
25492
25712
  const ext = arch === "aarch64" ? ".s" : ".c";
25493
25713
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
@@ -25573,8 +25793,8 @@ function report_file(result) {
25573
25793
  const mark = result.ok ? C.green("✓") : C.red("✗");
25574
25794
  console.log(` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`);
25575
25795
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
25576
- for (const f of result.fails) console.log(` ${C.red("✗")} ${C.bold(f.test)} ${C.dim(">")} ${f.message}`);
25577
- for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${C.bold(b.label)} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
25796
+ for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
25797
+ for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
25578
25798
  if (result.other.length) {
25579
25799
  console.log(C.dim(" --- test stdout ---"));
25580
25800
  for (const line of result.other) console.log(C.dim(` ${line}`));
@@ -25595,7 +25815,7 @@ function runTests(root, options = {}) {
25595
25815
  }
25596
25816
  const elapsed = performance.now() - startTime;
25597
25817
  const totalFiles = results.length;
25598
- const totalFilesFailed = results.reduce((a, r) => a + (r.fails ? 1 : 0), 0);
25818
+ const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
25599
25819
  const totalFilesPassed = totalFiles - totalFilesFailed;
25600
25820
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
25601
25821
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
@@ -25667,7 +25887,6 @@ function collect_nm_files(folder) {
25667
25887
  }
25668
25888
  return out;
25669
25889
  }
25670
- let build_root;
25671
25890
  console.error("\n~ NOMEN ~\n");
25672
25891
  const args = parse_args();
25673
25892
  const command = args.command;
@@ -25728,7 +25947,6 @@ try {
25728
25947
  }
25729
25948
  args.in = args.in ?? resolve_input();
25730
25949
  if (!args.in) process.exit(0);
25731
- if (!build_root) build_root = fs.lstatSync(args.in).isDirectory() ? args.in : path.dirname(args.in);
25732
25950
  if (fs.existsSync(args.in)) {
25733
25951
  let config = {
25734
25952
  arch: "aarch64",
@@ -25758,19 +25976,13 @@ function resolve_input() {
25758
25976
  if (fs.existsSync(config_path)) try {
25759
25977
  const json = fs.readFileSync(config_path, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
25760
25978
  const parsed = JSON.parse(json);
25761
- if (parsed.entry) {
25762
- build_root = cwd;
25763
- return path.resolve(cwd, parsed.entry);
25764
- }
25979
+ if (parsed.entry) return path.resolve(cwd, parsed.entry);
25765
25980
  } catch {}
25766
25981
  let nm_files = [];
25767
25982
  try {
25768
25983
  nm_files = fs.readdirSync(cwd).filter((f) => f.endsWith(".nm"));
25769
25984
  } catch {}
25770
- if (nm_files.length === 1) {
25771
- build_root = cwd;
25772
- return path.resolve(cwd, nm_files[0]);
25773
- }
25985
+ if (nm_files.length === 1) return path.resolve(cwd, nm_files[0]);
25774
25986
  if (nm_files.length > 1) {
25775
25987
  console.log(`Found ${nm_files.length} .nm files in ${cwd}. Specify which to run with --in:\n ` + nm_files.join("\n "));
25776
25988
  return;
@@ -25863,7 +26075,7 @@ function processFile(filename, config, mode) {
25863
26075
  return;
25864
26076
  }
25865
26077
  const basename = path.basename(filename, ".nm");
25866
- const buildDir = path.join(build_root ?? path.dirname(filename), "build");
26078
+ const buildDir = build_dir_for(resolved_path, filename.endsWith(".test.nm"));
25867
26079
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
25868
26080
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
25869
26081
  const headerfile = path.join(buildDir, "main.h");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { parse_args, print_help, type Args } from "./args.ts";
14
14
  import { run_docs } from "./docs.ts";
15
15
  import render_errors, { render_warnings } from "./format_errors.ts";
16
16
  import { find_bundled, run_init } from "./init.ts";
17
+ import { build_dir_for } from "./paths.ts";
17
18
  import { runTests } from "./test.ts";
18
19
  import type Config from "./types/Config.ts";
19
20
 
@@ -66,11 +67,6 @@ function collect_nm_files(folder: string): string[] {
66
67
  return out;
67
68
  }
68
69
 
69
- // The folder whose `build/` subdirectory receives compiler output. Set during
70
- // input resolution: the --in folder, the .nm file's folder, or — for
71
- // package.jsonc discovery — the package folder (cwd), not the entry's folder.
72
- let build_root: string | undefined;
73
-
74
70
  console.error("\n~ NOMEN ~\n");
75
71
 
76
72
  const args: Args = parse_args();
@@ -154,11 +150,6 @@ try {
154
150
  if (!args.in) {
155
151
  process.exit(0);
156
152
  }
157
- // For an explicit --in, build next to whatever was passed (the folder
158
- // itself, or the file's folder). Discovery cases set build_root themselves.
159
- if (!build_root) {
160
- build_root = fs.lstatSync(args.in).isDirectory() ? args.in : path.dirname(args.in);
161
- }
162
153
 
163
154
  if (fs.existsSync(args.in)) {
164
155
  let config: Config = { arch: "aarch64", platform: default_platform() };
@@ -213,7 +204,6 @@ function resolve_input(): string | undefined {
213
204
  const json = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
214
205
  const parsed = JSON.parse(json);
215
206
  if (parsed.entry) {
216
- build_root = cwd;
217
207
  return path.resolve(cwd, parsed.entry);
218
208
  }
219
209
  } catch {
@@ -229,7 +219,6 @@ function resolve_input(): string | undefined {
229
219
  // unreadable working folder
230
220
  }
231
221
  if (nm_files.length === 1) {
232
- build_root = cwd;
233
222
  return path.resolve(cwd, nm_files[0]);
234
223
  }
235
224
  if (nm_files.length > 1) {
@@ -368,7 +357,8 @@ function processFile(filename: string, config: Config, mode: Mode) {
368
357
  }
369
358
 
370
359
  const basename = path.basename(filename, ".nm");
371
- const buildDir = path.join(build_root ?? path.dirname(filename), "build");
360
+ // Output lives in the project's `build/` — `build/test` for *.test.nm.
361
+ const buildDir = build_dir_for(resolved_path, filename.endsWith(".test.nm"));
372
362
  if (!fs.existsSync(buildDir)) {
373
363
  fs.mkdirSync(buildDir, { recursive: true });
374
364
  }
package/src/init.ts CHANGED
@@ -26,7 +26,7 @@ const MAIN_NM = `import System
26
26
  pub func add = (int a, int b) => a + b
27
27
 
28
28
  pub func main = (Init init) {
29
- Console.write("Hello world, 2 + 2 is \\{add(a + b)}!\\n")
29
+ Console.write("Hello world, 2 + 2 is \\{add(2, 2)}!\\n")
30
30
  }
31
31
  `;
32
32
 
package/src/paths.ts ADDED
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * The project root for `input_path`: the nearest ancestor folder (or the input
6
+ * folder itself) containing a `package.jsonc`. Falls back to the input's own
7
+ * folder for standalone files, so builds stay next to their source.
8
+ */
9
+ export function project_root_for(input_path: string): string {
10
+ let dir = fs.lstatSync(input_path).isDirectory() ? input_path : path.dirname(input_path);
11
+ for (let i = 0; i < 20; i++) {
12
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ return path.dirname(input_path);
18
+ }
19
+
20
+ /**
21
+ * The folder that receives compiler output for `input_path`: the project's
22
+ * `build/`, or `build/test` for `*.test.nm` files.
23
+ */
24
+ export function build_dir_for(input_path: string, is_test: boolean): string {
25
+ return is_test
26
+ ? path.join(project_root_for(input_path), "build", "test")
27
+ : path.join(project_root_for(input_path), "build");
28
+ }
package/src/test.ts CHANGED
@@ -8,6 +8,7 @@ import join from "../../src/join.ts";
8
8
  import { get_library } from "../../src/lib.ts";
9
9
  import parse from "../../src/parse.ts";
10
10
  import { find_bundled } from "./init.ts";
11
+ import { build_dir_for } from "./paths.ts";
11
12
 
12
13
  const RECORD_PREFIX = "\\nomen|";
13
14
 
@@ -303,7 +304,7 @@ export function run_test_file(
303
304
  return result;
304
305
  }
305
306
 
306
- const buildDir = path.join(path.dirname(resolved), "build");
307
+ const buildDir = build_dir_for(resolved, true);
307
308
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
308
309
  const ext = arch === "aarch64" ? ".s" : ".c";
309
310
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
@@ -407,11 +408,11 @@ export function report_file(result: TestFileResult): void {
407
408
  // short-circuiting a test has at most one failure, so the fail lines are
408
409
  // the per-test detail and a separate count line would just repeat names.
409
410
  for (const f of result.fails) {
410
- console.log(` ${C.red("✗")} ${C.bold(f.test)} ${C.dim(">")} ${f.message}`);
411
+ console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
411
412
  }
412
413
  for (const b of result.benches) {
413
414
  console.log(
414
- ` ${C.cyan("⏱")} ${C.bold(b.label)} ${C.dim(`(n=${b.n})`)} ` +
415
+ ` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ` +
415
416
  `${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ` +
416
417
  `${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ` +
417
418
  `${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`,
@@ -447,7 +448,7 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
447
448
 
448
449
  const elapsed = performance.now() - startTime;
449
450
  const totalFiles = results.length;
450
- const totalFilesFailed = results.reduce((a, r) => a + (r.fails ? 1 : 0), 0);
451
+ const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
451
452
  const totalFilesPassed = totalFiles - totalFilesFailed;
452
453
  // `tests[].failed` (from each test's `done` record) is the authoritative
453
454
  // failure count; `result.fails` holds the same failures' messages for