Work on new Java based generator
authorNot Zed <notzed@gmail.com>
Mon, 28 Jul 2025 00:49:16 +0000 (10:19 +0930)
committerNot Zed <notzed@gmail.com>
Mon, 28 Jul 2025 00:49:16 +0000 (10:19 +0930)
14 files changed:
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/API.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Context.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Export.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Generator.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Template.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Tokeniser.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Util.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Value.java [new file with mode: 0644]
src/notzed.nativez.tools/classes/module-info.java [new file with mode: 0644]

diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/API.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/API.java
new file mode 100644 (file)
index 0000000..287266a
--- /dev/null
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/**
+ * Loads and processes an .api file.
+ * <p>
+ */
+public class API {
+
+       /* data loaded from api file */
+       List<LibraryInfo> libraries;
+       Deque<TemplateMap> types;
+       // Expanded code-map with key "name:field"
+       Map<String, Text> codes;
+       // Expanded system-map with key 'type:name'?  Is this useful?
+       Map<String, Text> system;
+
+       public API(List<LibraryInfo> system, List<LibraryInfo> libraries, Deque<TemplateMap> types, Collection<TemplateMap> codes) {
+               this.libraries = libraries;
+               this.types = types;
+               this.codes = new HashMap<>();
+               this.system = new HashMap<>();
+
+               // Codes are ordered in reverse, only take first entry to override others
+               for (var c: codes) {
+                       for (var e: c.fields.entrySet()) {
+                               this.codes.putIfAbsent(c.name + ":" + e.getKey(), e.getValue());
+                       }
+               }
+               for (var c: system) {
+                       for (var e: c.system.entrySet()) {
+                               this.system.put(c.name + ":" + e.getKey(), e.getValue());
+                       }
+               }
+       }
+
+       public Data code() {
+               return Data.ofTextMap(codes);
+       }
+
+       public Data code(String module) {
+               return Data.ofTextMap(codes, module + ":");
+       }
+
+       public Data system() {
+               return Data.ofTextMap(system);
+       }
+
+       public Data system(String part) {
+               return Data.ofTextMap(system, part + ":");
+       }
+
+       public Text getCode(String type, String name) {
+               return codes.getOrDefault(type + ":" + name, Text.UNDEFINED);
+       }
+
+       public Text getCode(Text key) {
+               switch (key.type()) {
+               case LITERAL:
+               case STRING:
+                       return key;
+               case IDENTIFIER:
+                       return codes.getOrDefault(key.value(), Text.UNDEFINED);
+               default:
+                       throw new NoSuchElementException("No key for :" + key);
+               }
+       }
+
+       public Text getSystem(Match.Type type, String key, Text fallback) {
+               var v = system.get(type.name() + ":" + key);
+               if (v != null)
+                       return v;
+               else
+                       return fallback;
+       }
+
+       public DependencyMap newDependencyMap(Export capi) {
+               return new DependencyMap(capi);
+       }
+
+       public class DependencyMap {
+               Set<String> seen = new HashSet<>();
+               Export capi;
+               // Stores expadned types
+               Map<String, TemplateMap> visited = new HashMap<>();
+
+               public DependencyMap(Export capi) {
+                       this.capi = capi;
+               }
+
+               public Stream<Hash> visisted(Match.Type type) {
+                       String match = type.name() + ":";
+                       return seen.stream().filter(s -> s.startsWith(match)).map(capi::getType);
+               }
+
+               // Maybe return all types in set (i.e.g api.visited) plus anon types (merge?)
+               public void visitDependencies(Set<String> roots) {
+                       Deque<Hash> queue = new ArrayDeque<>(roots.size());
+
+                       // Types from the start set
+                       for (String s: roots) {
+                               Hash h = capi.getType(s);
+                               assert h.defined();
+                               if (seen.add(s))
+                                       queue.add(h);
+                       }
+                       /*
+                       for (Value v: start.hash.values()) {
+                               Hash h = (Hash)v;
+                               System.out.println(h.getScalar("type").getValue() + ":" + h.getScalar("name").getValue());
+                               seen.add(h.getScalar("type").getValue() + ":" + h.getScalar("name").getValue());
+                               queue.add((Hash)v);
+                       }*/
+
+                       while (!queue.isEmpty()) {
+                               Hash type = queue.removeFirst();
+
+                               //System.out.println("\nprocess: " + type.getScalar("type").value + type.getScalar("name").value);
+
+                               //type.dump(System.out);
+                               //Export.allFields(type).forEach(h -> h.dump(System.out));
+
+                               Export.allFields(type)
+                                       .map(Hash.field("deref"))
+                                       .forEach(this::visitType);
+
+                               // What do i add to the queue?
+                               // What about all the types it added, how do look them up?
+                               // Field 'type' will map t capi->{key}
+                               Export.allFields(type)
+                                       .map(Hash.field("type"))
+                                       .filter(s -> s.indexOf(':') >= 0)
+                                       .filter(seen::add)
+                                       .filter(s -> {
+                                               Hash v = capi.types.getHash(s);
+                                               if (!v.defined()) {
+                                                       throw new RuntimeException("no type: " + s);
+                                               }
+                                               /*
+                                               if (!v.defined()) {
+                                                       if (s.startsWith("struct:")) {
+                                                               v = new Hash();
+                                                               v.put("type", s);
+                                                       } else {
+                                                               System.getLogger(API.class.getName()).log(System.Logger.Level.WARNING, () -> "Unsupported anonymous type: " + s);
+                                                       }
+                                               }*/
+                                               return v.defined();
+                                       })
+                                       .map(s -> capi.types.getHash(s))
+                                       .forEach(queue::add);
+                       }
+               }
+
+               /**
+                * Mark a type as used.
+                * This is used to build a concrete type map as well as indicate which types are
+                * in use.
+                *
+                * @param deref Expanded raw type name.
+                */
+               public void visitType(String deref) {
+                       Matcher matcher = null;
+                       TemplateMap matched = null;
+
+                       //System.out.printf("visit: %s\n", deref);
+
+                       if (visited.containsKey(deref))
+                               return;
+
+                       // Find the last matching type, exact matches override pattern matching.
+                       for (var type: types) {
+                               if (type.exact(deref)) {
+                                       System.out.println("add exact type: " + deref);
+                                       visited.put(deref, type);
+                                       return;
+                               } else if (matched == null) {
+                                       Matcher test = type.pattern.matcher(deref);
+
+                                       if (test.matches()) {
+                                               matcher = test;
+                                               matched = type;
+                                       }
+                               }
+                       }
+
+                       if (matcher != null && matched != null) {
+                               TemplateMap type = matched.copyAs(deref);
+
+                               for (var e: matcher.namedGroups().entrySet())
+                                       type.fields.put(e.getKey(), Text.ofLiteral(matcher.group(e.getValue())));
+
+                               System.out.println("add matching type: " + deref);
+                               visited.put(deref, type);
+                       } else {
+                               throw new NoSuchElementException("No match for type: " + deref);
+                       }
+               }
+
+               public Function<String, Data> typeMap() {
+                       // Copy all flags from types to instances of/pointers to types
+                       seen.stream().filter(s -> s.startsWith("struct:")).map(capi::getType).forEach(type -> {
+                               String name = type.getScalar("name").value;
+                               TemplateMap temp = visited.get("u64:${" + name + "}");
+                               if (temp != null) {
+                                       type.hash.entrySet().stream().filter(e -> e.getKey().endsWith("?"))
+                                               .forEach(e -> {
+                                                       temp.setTemplate(e.getKey(), Text.ofValue(e.getValue()));
+                                               });
+                               } else {
+                                       System.getLogger(API.class.getName()).log(System.Logger.Level.INFO, () -> "No pointer for anonymous type: " + name);
+                               }
+                       });
+
+                       return (key) -> {
+                               return visited.get(key);
+                       };
+               }
+       }
+
+       /**
+        * For storing code and type blocks.
+        * <p>
+        * TODO: templates can be pre-compiled, do that here on-demand?
+        */
+       static class TemplateMap implements Data {
+               String name;
+               Pattern pattern;
+               Map<String, Text> fields = new HashMap<>();
+
+               public TemplateMap(String name, boolean regex) {
+                       this.name = name;
+                       this.pattern = regex ? Pattern.compile(name) : null;
+               }
+
+               public TemplateMap(String name) {
+                       this(name, false);
+               }
+
+               public TemplateMap copyAs(String name) {
+                       TemplateMap map = new TemplateMap(name);
+                       map.fields.putAll(fields);
+                       return map;
+               }
+
+               public boolean exact(String name) {
+                       return pattern == null && this.name.equals(name);
+               }
+
+               public void setTemplate(String name, Text template) {
+                       fields.put(name, template);
+               }
+
+               // pre-parse here?
+               public Text getTemplate(String name) {
+                       return fields.get(name);
+               }
+
+               public void inherit(TemplateMap b) {
+                       fields.putAll(b.fields);
+               }
+
+               @Override
+               public Value getValue(Text key) {
+                       return getText(key).toValue();
+               }
+
+               @Override
+               public Text getText(Text key) {
+                       return fields.getOrDefault(key.value(), Text.UNDEFINED);
+               }
+
+               public void dump(PrintStream out) {
+                       out.printf("\ttype: %s\n", name);
+                       for (var e: fields.entrySet()) {
+                               out.printf("\t\t%s", e.getKey());
+                               switch (e.getValue().type()) {
+                               case IDENTIFIER ->
+                                       out.printf(" %s\n", e.getValue().value());
+                               case LITERAL ->
+                                       out.printf(" <[%s]>\n", e.getValue().value());
+                               case STRING ->
+                                       out.printf(" \"%s\";\n", e.getValue().value());
+                               }
+                       }
+               }
+       }
+
+       static record Option(String name, Text code) {
+               Option(Text code) {
+                       this("#flag", code);
+               }
+       }
+
+       static record Match(Type type, Predicate<String> matcher, List<Option> options, Text code) {
+
+               static Match ofPattern(Type type, String pattern, List<Option> options) {
+                       Text code;
+                       options.forEach(System.out::println);
+
+                       if (options.isEmpty())
+                               code = Text.ofLiteral("template:" + type.name());
+                       else {
+                               Option c = options.stream().filter(o -> o.name.equals("code")).findFirst().orElseGet(options::getLast);
+                               options.remove(c);
+                               code = c.code();
+                       }
+                       options.forEach(System.out::println);
+
+                       return new Match(type, Pattern.compile(pattern).asMatchPredicate(), List.copyOf(options), code);
+               }
+
+               enum Type {
+                       call,
+                       func,
+                       struct,
+                       union,
+                       // Only used for loading?
+                       load,
+                       option;
+               }
+
+               public Predicate<Hash> match(String field) {
+                       return h -> {
+                               Scalar name = h.getScalar(field);
+                               return name.defined() ? matcher().test(name.getValue()) : false;
+                       };
+               }
+
+       }
+
+       /**
+        * For a library.
+        * <ul>
+        * <li>func /pattern/ template-or-literal;
+        * <li>struct /pattern/ template-or-literal;
+        * <li>code template-or-literal;
+        * <li>option name value;
+        * </ul>
+        */
+       static class LibraryInfo {
+               String name;
+               List<String> load = new ArrayList<>();
+               // non-pattern fields
+               Map<String, Text> system = new HashMap<>();
+               // Patterns to match things to templates
+               List<Match> field = new ArrayList<>();
+
+               public LibraryInfo(String name) {
+                       this.name = name;
+               }
+
+               // Use system() ?
+               public Text getSystem(Match.Type type, String key, Text fallback) {
+                       var v = system.get(type.name() + ":" + key);
+                       if (v != null)
+                               return v;
+                       else
+                               return fallback;
+               }
+
+               public Data system() {
+                       return Data.ofTextMap(system);
+               }
+
+               public Data system(String type) {
+                       return Data.ofTextMap(system, type + ":");
+               }
+
+               void dump(PrintStream out) {
+                       out.printf("library: %s\n", name);
+                       for (var l: load)
+                               out.printf("  load %s\n", l);
+                       for (var m: field) {
+                               out.printf("  %s\n", m);
+                       }
+               }
+
+       }
+
+       static class APIParser implements AutoCloseable {
+               static final char[] assoc = {'=', '>'};
+               private final Tokeniser in;
+               private final Path base;
+
+               List<LibraryInfo> libraries = new ArrayList<>();
+               List<LibraryInfo> system = new ArrayList<>();
+               Deque<TemplateMap> typeClasses = new ArrayDeque<>();
+               Deque<TemplateMap> types = new ArrayDeque<>();
+               Deque<TemplateMap> codeClasses = new ArrayDeque<>();
+               Deque<TemplateMap> codes = new ArrayDeque<>();
+
+               public APIParser(Path base, Tokeniser in) {
+                       this.in = in;
+                       this.base = base;
+               }
+
+               enum Types {
+                       code,
+                       type,
+                       include,
+                       library,
+                       system;
+               }
+
+               Types getType(String key) throws IOException {
+                       try {
+                               return Types.valueOf(key);
+                       } catch (IllegalArgumentException x) {
+                               throw new IOException(in.fatalMessage("Invalid object type: " + key));
+                       }
+               }
+
+               @Override
+               public void close() throws IOException {
+                       in.close();
+               }
+
+               String identifier(int t) throws IOException {
+                       if (t != Tokeniser.TOK_IDENTIFIER)
+                               throw new IOException(in.fatalMessage(t, "Expeccting identifier"));
+                       return in.getText();
+               }
+
+               /**
+                * Read a list of strings until ';'.
+                * Strings are string or identifier.
+                *
+                * @return
+                * @throws IOException
+                */
+               List<String> stringList() throws IOException {
+                       List<String> list = new ArrayList<>();
+                       int t;
+                       while ((t = in.nextToken()) != -1 && t != ';') {
+                               switch (t) {
+                               case Tokeniser.TOK_IDENTIFIER:
+                               case Tokeniser.TOK_STRING_SQ:
+                               case Tokeniser.TOK_STRING_DQ:
+                                       list.add(in.getText());
+                                       break;
+                               default:
+                                       throw new IOException(in.fatalMessage(t, "Expecting STRING IDENTIFIER or ';'"));
+                               }
+                       }
+                       return list;
+               }
+
+               /**
+                * Looks for ';' and nothing else.
+                *
+                * @throws IOException
+                */
+               void emptyList() throws IOException {
+                       int t = in.nextToken();
+                       if (t != ';')
+                               throw new IOException(in.fatalMessage(t, "Expecting ';'"));
+               }
+
+               /**
+                * Load a 'type' specification.
+                * Applies inheritance as it goes.
+                *
+                * @throws IOException
+                */
+               TemplateMap loadType(Deque<TemplateMap> classes, Deque<TemplateMap> types) throws IOException {
+                       TemplateMap map;
+                       int t;
+                       String text;
+
+                       switch ((t = in.nextToken())) {
+                       case Tokeniser.TOK_REGEX:
+                               text = in.getText();
+                               map = new TemplateMap(in.getText(), true);
+                               types.add(map);
+                               break;
+                       case Tokeniser.TOK_IDENTIFIER:
+                               map = new TemplateMap(in.getText());
+                               classes.addFirst(map);
+                               break;
+                       default:
+                               throw new IOException(in.fatalMessage(t, "Expected name or regex"));
+                       }
+
+                       // options before {, inherit
+                       // do i need options?  or just inherit?
+                       while ((t = in.nextToken()) != -1 && t != '{') {
+                               String tmp;
+
+                               switch (t) {
+                               case Tokeniser.TOK_IDENTIFIER:
+                                       tmp = in.getText();
+                                       classes.stream().filter(c -> c.exact(tmp)).findFirst().ifPresent(map::inherit);
+                                       break;
+                               case ';':
+                                       return map;
+                               default:
+                                       throw new IOException(in.fatalMessage(t, "Expected name"));
+                               }
+                       }
+
+                       // Parse fields
+                       while ((t = in.nextToken()) != -1 && t != '}') {
+                               String key = identifier(t);
+
+                               // TODO: support options, do i need options?
+                               Text value = textValue();
+
+                               map.setTemplate(key, value);
+                       }
+                       return map;
+               }
+
+               Text textValue() throws IOException {
+                       int c = in.nextToken();
+                       Text text = in.getText(c);
+
+                       if (c == Tokeniser.TOK_QUOTED || c == Tokeniser.TOK_LITERAL)
+                               return text;
+
+                       c = in.nextToken();
+                       if (c != ';')
+                               throw new IOException(in.fatalMessage(c, "Expected ';'"));
+                       return text;
+               }
+
+               Text textOrNoValue() throws IOException {
+                       int c = in.nextToken();
+
+                       if (c == ';')
+                               return Text.ofLiteral("#true");
+
+                       Text text = in.getText(c);
+
+                       if (c == Tokeniser.TOK_QUOTED || c == Tokeniser.TOK_LITERAL)
+                               return text;
+
+                       c = in.nextToken();
+                       if (c != ';')
+                               throw new IOException(in.fatalMessage(c, "Expected ';'"));
+                       return text;
+               }
+
+               List<Option> optionList(int until) throws IOException {
+                       List<Option> list = new ArrayList<>();
+                       Text assign = null;
+                       Text value = null;
+                       int t;
+
+                       while ((t = in.nextToken()) != -1 && t != until) {
+                               if (t == '=') {
+                                       if (assign != null)
+                                               throw new IOException(in.fatalMessage("Broken assignment"));
+                                       assign = value;
+                                       value = null;
+                               } else {
+                                       Text text = in.getText(t);
+
+                                       if (assign != null) {
+                                               list.add(new Option(assign.value(), text));
+                                               assign = null;
+                                       } else if (value != null) {
+                                               list.add(new Option(value));
+                                               value = text;
+                                       } else {
+                                               value = text;
+                                       }
+                               }
+                       }
+
+                       if (assign != null)
+                               throw new IOException(in.fatalMessage("Broken assignment"));
+
+                       if (value != null)
+                               list.add(new Option(value));
+
+                       return list;
+               }
+
+               LibraryInfo loadLibrary(List<LibraryInfo> libs) throws IOException {
+                       int t;
+                       String name = switch ((t = in.nextToken())) {
+                               case Tokeniser.TOK_IDENTIFIER ->
+                                       in.getText();
+                               default ->
+                                       throw new IOException(in.fatalMessage(t, "Expected name"));
+                       };
+                       LibraryInfo lib = new LibraryInfo(name);
+
+                       // Library options   inherit?
+                       // Ignore for now
+                       while ((t = in.nextToken()) != -1 && t != '{') {
+                               if (t != Tokeniser.TOK_IDENTIFIER) {
+                                       throw new IOException(in.fatalMessage(t, "Expected name"));
+                               }
+                       }
+
+                       // Library fields
+                       while ((t = in.nextToken()) != -1 && t != '}') {
+                               Match.Type match = Match.Type.valueOf(identifier(t));
+
+                               switch (match) {
+                               case load ->
+                                       lib.load.addAll(stringList());
+                               case call, func, struct, union, option -> {
+                                       switch (t = in.nextToken()) {
+                                       case Tokeniser.TOK_REGEX ->
+                                               lib.field.add(Match.ofPattern(match, in.getText(), optionList(';')));
+                                       case Tokeniser.TOK_IDENTIFIER ->
+                                               lib.system.put(match.name() + ":" + in.getText(), textOrNoValue());
+                                       default ->
+                                               throw new IOException(in.fatalMessage(t, "Expecting regex or identifier"));
+                                       }
+                               }
+                               }
+                       }
+
+                       libs.add(lib);
+
+                       return lib;
+               }
+
+               API load() throws IOException {
+                       int t;
+
+types:      while ((t = in.nextToken()) != -1) {
+                               String name = identifier(t);
+                               Types type = getType(name);
+                               List<String> strings;
+
+                               switch (type) {
+                               case type:
+                                       loadType(typeClasses, types);
+                                       break;
+                               case code:
+                                       loadType(codeClasses, codes);
+                                       break;
+                               case include:
+                                       strings = stringList();
+                                       for (int i = 0; i < strings.size(); i++)
+                                               in.push(Files.newBufferedReader(base.resolveSibling(strings.get(strings.size() - 1 - i))));
+                                       break;
+                               case library:
+                                       loadLibrary(libraries);
+                                       break;
+                               case system:
+                                       loadLibrary(system);
+                                       break;
+                               }
+                       }
+
+                       return new API(system, libraries, types, codeClasses);
+               }
+
+       }
+
+       static API load(Path src) throws IOException {
+               try (APIParser p = new APIParser(src, new Tokeniser(Files.newBufferedReader(src)))) {
+                       return p.load();
+               }
+       }
+
+       public static void main(String[] args) throws IOException {
+               System.setProperty("java.util.logging.config.file", "logging.properties");
+               load(Path.of("test.api"));
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java
new file mode 100644 (file)
index 0000000..658d5e9
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Array extends Value {
+       static final Array UNDEFINED = new Array(List.of());
+       List<Value> array;
+
+       public Array() {
+               this(new ArrayList<>());
+       }
+
+       public Array(List<Value> array) {
+               this.array = array;
+       }
+
+       @Override
+       boolean defined() {
+               return this != UNDEFINED;
+       }
+
+       public Value getValue(int i) {
+               return defined() ? array.get(i) : Scalar.UNDEFINED;
+       }
+
+       public Array getArray(int start, int end) {
+               return new Array(array.subList(Math.min(array.size(), start), Math.min(array.size(), end)));
+       }
+
+       @Override
+       void dumper(StringBuilder sb, String indent) {
+               sb.append("[\n");
+               for (au.notzed.nativez.tools.Value v: array) {
+                       sb.append(indent);
+                       v.dumper(sb, indent + "\t");
+                       sb.append(",\n");
+               }
+               sb.append(indent);
+               sb.append("]");
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Context.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Context.java
new file mode 100644 (file)
index 0000000..e566efa
--- /dev/null
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/**
+ * Template processing context.
+ */
+public class Context implements Data {
+       Function<String, Data> typeMap;
+       Context parent;
+       Data data[];
+
+       public Context(Function<String, Data> typeMap, Data... data) {
+               this(typeMap, null, data);
+       }
+
+       private Context(Function<String, Data> typeMap, Context parent, Data... data) {
+               this.typeMap = typeMap;
+               this.parent = parent;
+               this.data = data;
+       }
+
+       private Context(Function<String, Data> typeMap, Context parent, List<Data> list) {
+               this(typeMap, parent, list.toArray(Data[]::new));
+       }
+
+       /**
+        * Resolves a key, expanding as necessary.
+        *
+        * FIXME: use same conventions as getValue()!
+        *
+        * e.g. use / for paths not .
+        * @param key
+        * @return
+        */
+       public String resolveText(String name) {
+               Text key = Text.ofIdentifier(name);
+               Text val = getText(key);
+
+               while (true) {
+                       switch (val.type()) {
+                       case IDENTIFIER:
+                               val = getText(val);
+                               break;
+                       case STRING:
+                               try (StringWriter dst = new StringWriter()) {
+                                       Template.load(val.value()).process(this, dst);
+                                       return dst.toString();
+                               } catch (IOException ex) {
+                                       System.getLogger(Context.class.getName()).log(System.Logger.Level.ERROR, (String)null, ex);
+                                       throw new RuntimeException();
+                               }
+                       case LITERAL:
+                               return val.value();
+                       case UNDEFINED: {
+                               if (key.value().indexOf('.') > 0) {
+                                       String path[] = key.value().split("\\.");
+                                       int i = 0;
+                                       Value base = getValue(Text.ofIdentifier(path[i++]));
+                                       while (true) {
+                                               if (base instanceof Scalar s) {
+                                                       // No way to know if it's a template so treat it as a string
+                                                       val = Text.ofString(s.getValue());
+                                                       break;
+                                               } else {
+                                                       if (i == path.length)
+                                                               throw new NoSuchElementException(name);
+                                                       switch (base) {
+                                                       case Hash h ->
+                                                               base = h.getValue(path[i++]);
+                                                       case Array a -> {
+                                                               base = a.getValue(Integer.parseInt(path[i++]));
+                                                       }
+                                                       default -> {
+                                                               throw new NoSuchElementException(name);
+                                                       }
+                                                       }
+                                               }
+                                       }
+                               } else {
+                                       throw new RuntimeException("No such key: " + key);
+                               }
+                               break;
+                       }
+                       default:
+                               throw new RuntimeException("inalid type: " + val);
+                       }
+               }
+       }
+
+       /**
+        * Range format.
+        * <p>
+        * start:end
+        * index
+        * <p>
+        * "0:4" number range exclusive
+        * "1" specific entry from start
+        * "-1" specific entry from end
+        * "3:-" skip start to end
+        * "-3:-" last 3 values
+        *
+        * @param key
+        * @return
+        */
+       static Value range(Array src, String key) {
+               int idx = key.indexOf(':');
+
+               System.out.println("key: " + key);
+               if (idx < 0) {
+                       idx = Integer.parseInt(key);
+                       if (idx < 0)
+                               return src.getValue(src.array.size() + idx);
+                       else
+                               return src.getValue(idx);
+               } else if (idx == 0 || idx == key.length() - 1) {
+                       throw new IllegalArgumentException("Invalid array reference: " + key);
+               }
+               int start = Integer.parseInt(key, 0, idx, 10);
+               int end = (idx == key.length() - 2 && key.charAt(idx + 1) == '-')
+                       ? src.array.size() - 1
+                       : Integer.parseInt(key, idx + 1, key.length(), 10);
+               start = start < 0 ? src.array.size() + start : start;
+               end = end < 0 ? src.array.size() + end : end;
+               return src.getArray(start, end + 1);
+       }
+
+       @Override
+       public Value getValue(Text key) {
+               // Only lookup identifiers
+               if (key.type() != Text.Type.IDENTIFIER)
+                       return key.toValue();
+
+               // Check path reference
+               if (key.value().indexOf('/') > 0) {
+                       String[] path = key.value().split("/");
+                       int idx = 0;
+                       Value val = getValue(Text.ofIdentifier(path[idx++]));
+
+                       while (idx < path.length) {
+                               if (val instanceof Hash h) {
+                                       val = h.getValue(path[idx++]);
+                               } else if (val instanceof Array a) {
+                                       val = range(a, path[idx++]);
+                               }
+                       }
+
+                       return val;
+               }
+
+               // code template reference (not really needed but for completeness sake)
+               //if (key.value().indexOf(':') > 0)
+               //      return api.getCode(key).toValue();
+               return Stream.of(data)
+                       .map(d -> d.getValue(key))
+                       .filter(t -> t.defined())
+                       .findFirst()
+                       .orElseGet(() -> parent != null ? parent.getValue(key) : Scalar.UNDEFINED);
+               /*                      .orElseGet(() -> {
+                               if (parent != null)
+                                       return parent.getValue(key);
+
+                               System.out.println("fallback key lookup " + key);
+                               Text val = api.getCode("default", key.value());
+                               System.out.println(" code: " + val);
+                               if (!val.defined())
+                                       val = api.getSystem(API.Match.Type.option, key.value(), Text.UNDEFINED);
+                               System.out.println(" option: " + val);
+                               return val.toValue();
+
+                       });*/
+       }
+
+       public void dump(PrintStream out) {
+               System.out.printf("dump %s, parent %s\n", this, parent);
+
+               Context c = this;
+               while (c != null) {
+                       for (Data d: c.data)
+                               d.dump(System.out);
+                       c = c.parent;
+                       System.out.printf(" parent %s\n", c);
+               }
+       }
+
+       @Override
+       public Text getText(Text key) {
+               while (key.type() == Text.Type.IDENTIFIER) {
+                       Text query = key;
+
+                       key = Stream.of(data)
+                               .map(d -> d.getText(query))
+                               .filter(t -> t.defined())
+                               .findFirst()
+                               .orElseGet(() -> parent != null ? parent.getText(query) : Text.UNDEFINED);
+               }
+
+               return key;
+
+               // code template reference
+               //if (key.value().indexOf(':') > 0)
+               //      return api.getCode(key);
+               /*
+                                       if (parent != null)
+                                               return parent.getText(key);
+                                       Text val = api.getCode("default", key.value());
+                                       if (!val.defined())
+                                               val = api.getSystem(API.Match.Type.option, key.value(), Text.UNDEFINED);
+                                       return val;
+                */
+       }
+
+       public Context push(Data... data) {
+               Text key = Text.ofIdentifier("deref");
+
+               List<Data> list = new ArrayList<>();
+               for (var d: data) {
+                       Value v = d.getValue(key);
+
+                       list.add(d);
+
+                       if (v.defined() && v instanceof Scalar s) {
+                               Data t = typeMap.apply(s.getValue());
+                               if (t != null) {
+                                       list.add(t);
+                               } else {
+                                       System.getLogger(Context.class.getName()).log(System.Logger.Level.WARNING, () -> "No type expansion defined for: " + s);
+                                       throw new AssertionError("API not properly initialised");
+                               }
+                       }
+               }
+
+               return new Context(typeMap, this, list);
+       }
+
+       /*
+       static final Context ROOT = new Context(null, null) {
+               @Override
+               protected Value getValue(String name) {
+                       // sigh, should it all be the same?
+                       return Scalar.UNDEFINED;
+               }
+
+               @Override
+               protected Scalar getScalar(String name) {
+                       return Scalar.UNDEFINED;
+               }
+
+               @Override
+               protected Array getArray(String name) {
+                       return Array.UNDEFINED;
+               }
+
+               @Override
+               protected Hash getHash(String name) {
+                       return Hash.UNDEFINED;
+               }
+
+       };
+        */
+ /*
+       @Deprecated
+       public Context(Context parent, Hash hash, API api) {
+               this.parent = parent;
+               this.hash = hash;
+               this.api = api;
+       }
+
+       @Deprecated
+       public Context(Hash hash, API api) {
+               this(ROOT, hash, api);
+       }
+       @Deprecated
+       public Context pull() {
+               return parent;
+       }
+
+       @Deprecated
+       public Context push(Hash hash) {
+               Scalar deref = hash.getScalar("deref");
+               if (deref.defined()) {
+                       template = api.visited.get(deref.getValue());
+               }
+               return new Context(this, hash, api);
+       }
+        */
+ /*
+       @Deprecated
+       protected Value getValue(String name) {
+               au.notzed.nativez.tools2.Value val = hash.getValue(name);
+               return val.defined() ? val : parent.getValue(name);
+       }
+
+       @Deprecated
+       protected Scalar getScalar(String name) {
+               au.notzed.nativez.tools2.Scalar val = hash.getScalar(name);
+               return val.defined() ? val : parent.getScalar(name);
+       }
+
+       @Deprecated
+       protected Array getArray(String name) {
+               au.notzed.nativez.tools2.Array val = hash.getArray(name);
+               return val.defined() ? val : parent.getArray(name);
+       }
+
+       @Deprecated
+       protected Hash getHash(String name) {
+               au.notzed.nativez.tools2.Hash val = hash.getHash(name);
+               return val.defined() ? val : parent.getHash(name);
+       }
+        */
+ /* Ressolve the base type */
+ /*
+       @Deprecated
+       public Value resolveValue(Text name) {
+               assert (name.type() == Text.Type.IDENTIFIER);
+               return getValue(name.value());
+       }
+
+       @Deprecated
+       public List<Value> resolveArray(Text name) {
+               assert (name.type() == Text.Type.IDENTIFIER);
+               return getArray(name.value()).array;
+       }
+
+       @Deprecated
+       public Text resolve(Text name) {
+               //System.out.println("resolving " + name);
+
+               if (name.type() != Text.Type.IDENTIFIER)
+                       return name;
+
+               // code template reference
+               if (name.value().indexOf(':') > 0)
+                       return api.getCode(name.value());
+
+               // Check local context
+               if (hash != null) {
+                       au.notzed.nativez.tools2.Scalar val = hash.getScalar(name.value());
+                       if (val.defined())
+                               return Text.ofString(val.getValue());
+               }
+
+               // Then template
+               if (template != null) {
+                       var tmp = template.getTemplate(name.value());
+                       if (tmp != null)
+                               return tmp;
+               }
+
+               // Try parent, TODO: loop here (void re-checking type/api test)
+               if (parent != null)
+                       return parent.resolve(name);
+               else
+                       return Text.ofLiteral("<undefined>");
+       }
+        */
+       /**
+        * Cast a value to a boolean.
+        * <p>
+        * A value must be defined, an array must be non-empty, and a scalar must not be '0' to be considered true.
+        *
+        * @param name
+        * @return
+        */
+       public boolean resolveBoolean(Text name) {
+               assert (name.type() == Text.Type.IDENTIFIER);
+
+               Value val = getValue(name);
+
+               if (val.defined()) {
+                       if (val instanceof Array array) {
+                               return !array.array.isEmpty();
+                       } else if (val instanceof Scalar scalar) {
+                               return !scalar.value.equals("0");
+                       }
+                       return true;
+               }
+               return false;
+       }
+
+       public int resolveCompare(Text name, Text value) {
+               assert (name.type() == Text.Type.IDENTIFIER);
+
+               // Could also resolve 'value' if it's an identifier i guess
+               String test = resolveText(name.value());
+               // TODO: numbers?
+               return test.compareTo(value.value());
+
+               /*
+               Text test = getText(name);
+
+               // Check for compound names
+               // Shit, this needs to be in getXX functions too.
+               // double shit, names have . in them.
+               if (!test.defined()) {
+                       String path[] = name.value().split("\\.");
+                       if (path.length > 1) {
+                               int i = 0;
+                               Value base = getValue(Text.ofIdentifier(path[i++]));
+                               while (true) {
+                                       if (base instanceof Hash h) {
+                                               base = h.getValue(path[i++]);
+                                       } else if (base instanceof Array a) {
+                                               base = a.getValue(Integer.parseInt(path[i++]));
+                                       } else if (base instanceof Scalar s) {
+                                               test = Text.ofValue(base);
+                                               break;
+                                       }
+                               }
+                       }
+               }
+
+               System.out.printf("compare '%s' <> '%s' - %s\n", name, value, test);
+
+               // TODO: numbers?
+               return test.value().compareTo(value.value());
+                */
+       }
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java
new file mode 100644 (file)
index 0000000..fc191f9
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.PrintStream;
+import java.util.Map;
+
+/* to replace context datas once i get it working */
+public interface Data {
+
+       /*
+       requirements
+
+       template processing
+        - foreach needs array or hash
+        - boolean needs 'defined', array-length/hash-size or scalar value
+        - compare needs scalar value
+        - append needs scalar value
+        */
+       public default Value getValue(Text key) {
+               return getText(key).toValue();
+       }
+
+       public default Text getText(Text key) {
+               if (key.defined()) {
+                       if (key.type() != Text.Type.IDENTIFIER)
+                               return key;
+
+                       else if (getValue(key) instanceof Scalar s && s.defined())
+                               return Text.ofString(s.getValue());
+               }
+               return Text.UNDEFINED;
+       }
+
+       public static Data ofTextMap(Map<String, Text> map) {
+               return new Data() {
+                       @Override
+                       public Text getText(Text key) {
+                               return map.getOrDefault(key.value(), Text.UNDEFINED);
+                       }
+
+                       @Override
+                       public void dump(PrintStream out) {
+                               for (var e: map.entrySet()) {
+                                       out.printf("  %s = %s\n", e.getKey(), e.getValue());
+                               }
+                       }
+               };
+       }
+
+       public static Data ofTextMap(Map<String, Text> map, String prefix) {
+               return new Data() {
+                       @Override
+                       public Text getText(Text key) {
+                               return map.getOrDefault(prefix + key.value(), Text.UNDEFINED);
+                       }
+
+                       @Override
+                       public void dump(PrintStream out) {
+                               for (var e: map.entrySet()) {
+                                       if (e.getKey().startsWith(prefix))
+                                               out.printf("  %s = %s\n", e.getKey().substring(prefix.length()), e.getValue());
+                               }
+                       }
+               };
+       }
+
+       public void dump(PrintStream out);
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Export.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Export.java
new file mode 100644 (file)
index 0000000..3492e19
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/**
+ * Represents the gcc exporter format.
+ * <p>
+ */
+public class Export {
+
+       /**
+        * All the types in the perl export.
+        * This is a hash of hashes, the hash contents depends on '.type'.
+        */
+       Hash types;
+
+       public Export(Hash types) {
+               this.types = types;
+
+               // Do some processing to fix up some things missing from the export
+               Set<String> all = new TreeSet<>();
+
+               for (Value ti: types.hash.values()) {
+                       Hash type = (Hash)ti;
+                       String klass = type.getScalar("type").value;
+                       Stream<Value> members;
+
+                       switch (klass) {
+                       case "struct":
+                               prepareStruct(type);
+                               members = type.getArray("fields").array.stream();
+                               break;
+                       case "union":
+                               // FIXME: implement
+                               System.getLogger(Export.class.getName()).log(System.Logger.Level.WARNING, () -> "Union unimplemented: " + type.getScalar("name").value);
+                               members = type.getArray("fields").array.stream();
+                               break;
+                       case "func":
+                               prepareFunc(type);
+                               members = Stream.concat(Stream.of(type.getValue("result")), type.getArray("arguments").array.stream());
+                               break;
+                       default:
+                               members = Stream.empty();
+                               break;
+                       }
+                       members.map(v -> (Hash)v).map(m -> {
+                               if (m.getScalar("type") == Scalar.UNDEFINED)
+                                       throw new RuntimeException("Missing type:\n" + m.dumper());
+                               return m.getScalar("deref").value;
+                       }).forEach(all::add);
+               }
+
+               for (String ref: all) {
+                       Matcher m = isPointer.matcher(ref);
+
+                       if (m.matches()) {
+                               String name = m.group("pointer");
+                               Hash s = types.getHash("struct:" + name);
+
+                               if (s == Hash.UNDEFINED) {
+                                       Hash handle = new Hash();
+
+                                       System.getLogger(Export.class.getName()).log(System.Logger.Level.INFO, () -> "Insert anonymous type: " + name);
+
+                                       handle.put("type", "struct");
+                                       handle.put("name", name);
+                                       handle.put("Name", renameField.apply(new Scalar(name)));
+                                       handle.put("opaque?", "#true");
+                                       handle.put("fields.doc", ("typedef struct " + name + " * " + name + ";"));
+
+                                       types.put("struct:" + name, handle);
+                               }
+                       }
+               }
+       }
+
+       public Hash getType(String typename) {
+               return types.getHash(typename);
+       }
+
+       public Hash getType(String type, String name) {
+               return getType(type + ":" + name);
+       }
+
+       public Stream<Hash> types(String type) {
+               return types.hash.values().stream()
+                       .map(v -> (Hash)v)
+                       .filter(v -> v.getScalar("type").getValue().equals(type));
+       }
+
+       public static Stream<Hash> allFields(Hash def) {
+               return (switch (def.getScalar("type").getValue()) {
+                       case "struct" ->
+                               def.getArray("fields").defined() ? def.getArray("fields").array.stream() : Stream.empty();
+                       case "func", "call" ->
+                                       Stream.concat(Stream.of(def.getHash("result")), def.getArray("arguments").array.stream());
+                       default ->
+                               Stream.empty();
+               }).map(n -> (Hash)n);
+       }
+
+       /**
+        * Scan all entriees of all fields.
+        *
+        * @return
+        */
+       public Stream<Hash> allFields() {
+               return types.hash.values().stream()
+                       .map(v -> (Hash)v)
+                       .flatMap(def -> {
+                               return switch (def.getScalar("type").getValue()) {
+                                       case "struct" ->
+                                               def.getHash("fields").defined() ? def.getHash("fields").hash.values().stream() : Stream.empty();
+                                       case "func", "call" ->
+                                               Stream.concat(Stream.of(def.getHash("result")), def.getArray("arguments").array.stream());
+                                       default ->
+                                               Stream.empty();
+                               };
+                       })
+                       .map(n -> (Hash)n);
+       }
+
+       Function<Scalar, Scalar> renameStruct = s -> new Scalar(Util.toStudlyCaps(s.getValue()));
+       Function<Scalar, Scalar> renameField = s -> new Scalar(Util.toCamelCase(s.getValue()));
+
+       static final Pattern isPointer = Pattern.compile("^u64:\\$\\{(?<pointer>.*)\\}$");
+       static final Pattern isStruct = Pattern.compile("^\\$\\{(?<struct>.*)\\}$");
+
+       final void prepareFunc(Hash f) {
+               Matcher m = isStruct.matcher(f.getHash("result").getScalar("deref").getValue());
+
+               if (m.matches()) {
+                       f.hash.put("struct-return?", new Scalar("#true"));
+               }
+       }
+
+       /**
+        * Initialise struct for template generation.
+        * Adds padding information and java-names.
+        */
+       final void prepareStruct(Hash s) {
+               s.hash.computeIfAbsent("Name", k -> renameStruct.apply(s.getScalar("name")));
+               if (!s.getArray("fields").defined())
+                       return;
+
+               StringBuilder doc = new StringBuilder();
+               Array list = new Array();
+               long offset = 0;
+
+               doc.append("struct ");
+               doc.append(s.getScalar("name"));
+               doc.append(" {\n");
+
+               for (Value fv: s.getArray("fields").array) {
+                       Hash f = (Hash)fv;
+                       long fsize = f.getScalar("size").getInteger();
+                       long foffset = f.getScalar("offset").getInteger();
+
+                       f.hash.computeIfAbsent("Name", k -> renameStruct.apply(f.getScalar("name")));
+
+                       if ((fsize & 7) != 0 || (foffset & 7) != 0) {
+                               System.getLogger("Generator").log(System.Logger.Level.WARNING, () -> "Bitfields not supported: " + s.getScalar("name"));
+                               continue;
+                       }
+
+                       if (foffset > offset) {
+                               Hash pad = new Hash();
+
+                               pad.hash.put("size", new Scalar(String.valueOf(foffset - offset)));
+                               pad.hash.put("offset", new Scalar(String.valueOf(foffset)));
+                               pad.hash.put("deref", new Scalar("padding"));
+                               list.array.add(pad);
+                       }
+                       list.array.add(f);
+
+                       offset = foffset + fsize;
+
+                       doc.append("\t\t");
+                       doc.append(f.getScalar("ctype"));
+                       doc.append(" ");
+                       doc.append(f.getScalar("name"));
+                       doc.append(";\n");
+               }
+
+               s.hash.put("fields.layout", list);
+
+               doc.append("\t}");
+               s.hash.put("fields.doc", new Scalar(doc.toString()));
+       }
+
+       static Export load(Path src) throws IOException {
+               return new Export((Hash)Value.load(src));
+       }
+
+       public static void main(String[] args) throws IOException {
+               System.setProperty("java.util.logging.config.file", "logging.properties");
+               load(Path.of("../notzed.dev/api/api.pm"));
+       }
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Generator.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Generator.java
new file mode 100644 (file)
index 0000000..e7e82cf
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import static au.notzed.nativez.tools.API.Match.Type.struct;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Generator {
+
+       Path dir;
+
+       public Generator(Path dir) {
+               this.dir = dir;
+       }
+
+       void generate(Context ctx, Template code) throws IOException {
+               String name = ctx.resolveText("Name");
+               String module = ctx.resolveText("module");
+               String[] pkg = ctx.resolveText("package").split("/");
+               Path path = dir.resolve(module).resolve("classes", pkg);
+               Path file = path.resolve(name + ".java");
+
+               Files.createDirectories(path);
+
+               try (var dst = Files.newBufferedWriter(file)) {
+                       System.out.printf("Generate %s\n", file);
+                       code.process(ctx, dst);
+               } catch (IOException ex) {
+                       System.getLogger(Generator.class.getName()).log(System.Logger.Level.ERROR, (String)null, ex);
+               }
+       }
+
+       void proto() throws IOException {
+               Path base = Path.of("api");
+               API api = API.load(base.resolve("test.api"));
+               Export capi = Export.load(base.resolve("test.pm"));
+
+               // Find all the types we're going to use
+               for (var lib: api.libraries) {
+                       Set<String> roots = new HashSet<>();
+
+                       lib.dump(System.out);
+
+                       // move to Library?
+                       // Get the base set
+                       for (var m: lib.field) {
+                               switch (m.type()) {
+                               case func:
+                               case struct:
+                               case call:
+                                       capi.types(m.type().name())
+                                               .filter(m.match("name"))
+                                               .forEach(f -> {
+                                                       // Set the call template as a new template (put this outside the loop?)
+                                                       Text code = m.code();
+                                                       if (code.type() == Text.Type.IDENTIFIER)
+                                                               f.hash.put(m.type().name() + ".template", new Scalar("{/" + code.value() + "}"));
+                                                       else
+                                                               f.hash.put(m.type().name() + ".template", code.toValue());
+                                                       for (API.Option o: m.options()) {
+                                                               if (o.name().equals("#flag")) {
+                                                                       f.put(o.code().value(), "#true");
+                                                               } else {
+                                                                       f.put(o.name(), o.code().toValue());
+                                                               }
+                                                       }
+                                                       roots.add(m.type().name() + ":" + f.getScalar("name").getValue());
+                                               });
+                                       break;
+
+                               default:
+                                       System.getLogger("Generator").log(System.Logger.Level.WARNING, () -> "Unhandled library entries: " + m);
+                               }
+                       }
+
+                       // Move to API, return a new structure?
+                       var view = api.newDependencyMap(capi);
+
+                       view.visitDependencies(roots);
+                       var typeMap = view.typeMap();
+
+                       // structures
+                       if (true) {
+                               // To allow overrriding per-struct would need to be pre-struct
+                               Context ctx = new Context(typeMap, lib.system(), lib.system("option"), api.code(), api.system("option"), api.system("default"), api.code("default"));
+
+                               for (String a: view.seen) {
+                                       //      if (anon.contains(a))
+                                       //              continue;
+
+                                       if (a.startsWith("struct:")) {
+                                               Hash type = capi.getType(a);
+
+                                               var sub = ctx.push(type);
+
+                                               //System.out.printf("struct context\n");
+                                               //sub.dump(System.out);
+
+                                               Template code = Template.load(sub.getText(Text.ofIdentifier("struct.template")).value());
+
+                                               generate(sub, code);
+                                       }
+                               }
+                       }
+
+                       // The library class itself
+                       if (true) {
+                               Hash info = new Hash();
+
+                               info.hash.put("name", new Scalar(lib.name));
+                               info.hash.put("Name", new Scalar(lib.name));
+
+                               // Translate from api library to table format
+                               Array funcList = new Array();
+
+                               view.visisted(API.Match.Type.func).forEach(funcList.array::add);
+
+                               //funcList.array.addAll(funcs.hash.values());
+                               //funcList.array.add(funcs.hash.get("manifold_simple_polygon_get_point"));
+                               //funcList.array.add(funcs.hash.get("manifold_destruct_simple_polygon"));
+                               info.hash.put("func", funcList);
+
+                               Array loadList = new Array();
+                               for (String l: lib.load) {
+                                       Hash entry = new Hash();
+                                       entry.hash.put("name", new Scalar(l));
+                                       loadList.array.add(entry);
+                               }
+                               info.hash.put("load", loadList);
+
+                               Context ctx = new Context(typeMap, info, lib.system(), lib.system("option"), api.code(), api.system("option"), api.system("default"), api.code("default"));
+                               Template code = Template.load(ctx.getText(Text.ofIdentifier("library.template")).value());
+
+                               generate(ctx, code);
+                       }
+                       // module-info
+                       if (false) {
+                               Context ctx = new Context(typeMap, lib.system(), api.system(), api.code(), api.code("default"));
+                               Template code = Template.load(ctx.getText(Text.ofIdentifier("module:info")).value());
+
+                               Hash info = new Hash();
+
+                               info.put("Name", "module-info");
+                               info.put("package", "");
+
+                               System.out.println(ctx.getText(Text.ofIdentifier("module:info")));
+
+                               StringWriter dst = new StringWriter();
+                               code.process(ctx.push(info), dst);
+                               System.out.println(dst);
+
+                               generate(ctx.push(info), code);
+                       }
+               }
+       }
+
+       public static void main(String[] args) throws IOException {
+               new Generator(Path.of("bin/gen")).proto();
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java
new file mode 100644 (file)
index 0000000..f98a198
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class Hash extends Value implements Data {
+       static final Hash UNDEFINED = new Hash(null);
+       Map<String, Value> hash;
+
+       public Hash() {
+               this(new HashMap<>());
+       }
+
+       private Hash(Map<String, Value> hash) {
+               this.hash = hash;
+       }
+
+       @Override
+       boolean defined() {
+               return this != UNDEFINED;
+       }
+
+       @Override
+       void dumper(StringBuilder sb, String indent) {
+               sb.append("{\n");
+               for (java.util.Map.Entry<java.lang.String, au.notzed.nativez.tools.Value> e: hash.entrySet()) {
+                       sb.append(indent);
+                       sb.append(e.getKey());
+                       sb.append(" => ");
+                       e.getValue().dumper(sb, indent + "\t");
+                       sb.append(",\n");
+               }
+               sb.append(indent);
+               sb.append("}");
+       }
+
+       @Override
+       public void dump(PrintStream out) {
+               out.print(super.dumper());
+       }
+
+       public static Function<Hash, String> field(String name) {
+               return f -> {
+                       Scalar value = f.getScalar(name);
+                       if (!value.defined())
+                               throw new AssertionError("missing field: " + name + " in " + f.dumper(), null);
+                       return value.getValue();
+               };
+       }
+
+       public Value getValue(String key) {
+               if (defined() && hash.get(key) instanceof Value s) {
+                       return s;
+               } else {
+                       return Scalar.UNDEFINED;
+               }
+       }
+
+       public Scalar getScalar(String key) {
+               if (defined() && hash.get(key) instanceof Scalar s) {
+                       return s;
+               } else {
+                       return Scalar.UNDEFINED;
+               }
+       }
+
+       public Array getArray(String key) {
+               if (defined() && hash.get(key) instanceof Array s) {
+                       return s;
+               } else {
+                       return Array.UNDEFINED;
+               }
+       }
+
+       public Hash getHash(String key) {
+               if (defined() && hash.get(key) instanceof Hash s) {
+                       return s;
+               } else {
+                       return Hash.UNDEFINED;
+               }
+       }
+
+       @Override
+       public Value getValue(Text key) {
+               if (defined() && key.defined() && hash.get(key.value()) instanceof Value s) {
+                       return s;
+               } else {
+                       return Scalar.UNDEFINED;
+               }
+       }
+
+       public void put(String key, String value) {
+               hash.put(key, new Scalar(value));
+       }
+
+       public void put(String key, Value value) {
+               hash.put(key, value);
+       }
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java
new file mode 100644 (file)
index 0000000..bd04116
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package au.notzed.nativez.tools;
+
+public class Scalar extends Value {
+       static final Scalar UNDEFINED = new Scalar(null);
+       String value;
+
+       public Scalar(String value) {
+               this.value = value;
+       }
+
+       @Override
+       boolean defined() {
+               return this != UNDEFINED;
+       }
+
+       public String getValue() {
+               return value;
+       }
+
+       public long getInteger() {
+               return value != null ? Long.parseLong(value) : 0;
+       }
+
+       @Override
+       void dumper(StringBuilder sb, String indent) {
+               sb.append(value);
+       }
+
+       @Override
+       public String toString() {
+               return value == null ? "<undefined>" : value;
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Template.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Template.java
new file mode 100644 (file)
index 0000000..a5698f3
--- /dev/null
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Pre-parsed template.
+ */
+public class Template {
+       List<Command> seq;
+
+       public Template(List<Command> list) {
+               seq = List.copyOf(list);
+       }
+
+       public void dump(PrintStream out) {
+               out.println("template");
+               seq.forEach(c -> {
+                       out.println(c.type);
+                       for (var v: c.values)
+                               out.printf("\t%s\n", v);
+               });
+       }
+
+       private void append(Context ctx, Writer dst, Text txt) throws IOException {
+               var val = ctx.getText(txt);
+
+               // TODO: maybe this should resolve things like ctx.resolveText() does
+               //ctx.resolveText(txt);
+
+               switch (val.type()) {
+               case IDENTIFIER:
+               case LITERAL:
+                       dst.append(val.value());
+                       break;
+               case STRING:
+                       Template tmp = load(val.value());
+                       tmp.process(ctx, dst);
+                       break;
+               case UNDEFINED:
+                       System.getLogger(Template.class.getName()).log(System.Logger.Level.ERROR, () -> "append '/" + txt + "' doesn't exist");
+                       break;
+               }
+       }
+
+       public void process(Context ctx, Writer dst) throws IOException {
+               for (Command cmd: seq) {
+                       switch (cmd.type) {
+                       case LITERAL:
+                               for (var v: cmd.values)
+                                       dst.append(v.value());
+                               break;
+                       case ISFALSE:
+                               if (!ctx.resolveBoolean(cmd.values[0])) {
+                                       append(ctx, dst, cmd.values[1]);
+                               } else if (cmd.values.length == 3) {
+                                       append(ctx, dst, cmd.values[2]);
+                               }
+                               break;
+                       case INVOKE:
+                               // Nothing yet
+                               System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "&command not implemented:    " + cmd.values[0]);
+                               break;
+                       case APPEND:
+                               append(ctx, dst, cmd.values[0]);
+                               break;
+                       case CMPLT:
+                               if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) < 0) {
+                                       append(ctx, dst, cmd.values[2]);
+                               } else if (cmd.values.length == 4) {
+                                       append(ctx, dst, cmd.values[3]);
+                               }
+                               break;
+                       case CMPEQ:
+                               if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) == 0) {
+                                       append(ctx, dst, cmd.values[2]);
+                               } else if (cmd.values.length == 4) {
+                                       append(ctx, dst, cmd.values[3]);
+                               }
+                               break;
+                       case CMPNE:
+                               if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) != 0) {
+                                       append(ctx, dst, cmd.values[2]);
+                               } else if (cmd.values.length == 4) {
+                                       append(ctx, dst, cmd.values[3]);
+                               }
+                               break;
+                       case CMPGT:
+                               if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) > 0) {
+                                       append(ctx, dst, cmd.values[2]);
+                               } else if (cmd.values.length == 4) {
+                                       append(ctx, dst, cmd.values[3]);
+                               }
+                               break;
+                       case ISTRUE:
+                               if (ctx.resolveBoolean(cmd.values[0])) {
+                                       append(ctx, dst, cmd.values[1]);
+                               } else if (cmd.values.length == 3) {
+                                       append(ctx, dst, cmd.values[2]);
+                               }
+                               break;
+                       case FOREACH: {
+                               Value val = ctx.getValue(cmd.values[0]);
+
+                               //System.out.printf("foreach template: %s %s -> %s\n", cmd.values[0], cmd.values[1], val);
+                               if (val instanceof Array array) {
+                                       boolean first = true;
+                                       for (au.notzed.nativez.tools.Value v: array.array) {
+                                               Context c = ctx.push((Data)v);
+                                               var key = c.getText(cmd.values[1]);
+
+                                               Template tmpl = load(key.value());
+
+                                               if (!first && cmd.values.length > 2) {
+                                                       assert (cmd.values[2].type() == Text.Type.LITERAL);
+                                                       dst.append(cmd.values[2].value());
+                                               }
+                                               first = false;
+
+                                               tmpl.process(c, dst);
+                                       }
+                               } else if (val instanceof Hash hash) {
+                                       //System.out.printf("@ over hash\n");
+                                       //hash.dump(System.out);
+                                       Context c = ctx.push((Data)hash);
+                                       var key = c.getText(cmd.values[1]);
+                                       Template tmpl = load(key.value());
+
+                                       tmpl.process(c, dst);
+                               } else {
+                                       System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "@foreach Cannot iterate over scalar: " + (val.defined() ? (Object)val : (Object)cmd.values[0]));
+                               }
+                               break;
+                       }
+                       }
+               }
+       }
+
+       static Template load(String src) throws IOException {
+               try (TemplateParser p = new TemplateParser(new TemplateTokeniser(Reader.of(src)))) {
+                       return p.load();
+               }
+       }
+
+       static Template load(Path src) throws IOException {
+               try (TemplateParser p = new TemplateParser(src, new TemplateTokeniser(Files.newBufferedReader(src)))) {
+                       return p.load();
+               }
+       }
+
+       static record Command(Type type, Text[] values) {
+
+               /**
+                * Command type.
+                * Note: After LITERAL order MUST match COMMANDS array & sorted ASCII order.
+                */
+               enum Type {
+                       LITERAL {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return true;
+                               }
+
+                       },
+                       ISFALSE {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validBoolean(args);
+                               }
+                       },
+                       INVOKE {
+                       },
+                       APPEND {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validKey(args);
+                               }
+
+                       },
+                       CMPLT {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validCompare(args);
+                               }
+                       },
+                       CMPEQ {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validCompare(args);
+                               }
+                       },
+                       CMPGT {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validCompare(args);
+                               }
+                       },
+                       ISTRUE {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validBoolean(args);
+                               }
+                       },
+                       FOREACH {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       // {@key temp thing}
+                                       return super.valid(args) && args.length >= 2 && args.length <= 3;
+                               }
+                       },
+                       CMPNE {
+                               @Override
+                               boolean valid(Text[] args) {
+                                       return validCompare(args);
+                               }
+                       };
+
+                       // Must match above and be in sorted-valued order
+                       static final char COMMANDS[] = {
+                               '!', '&', '/', '<', '=', '>', '?', '@', '~'
+                       };
+
+                       static Type valueOf(char c) {
+                               int id = Arrays.binarySearch(COMMANDS, c);
+                               if (id < 0)
+                                       return LITERAL;
+                               else
+                                       return values()[id + 1];
+                       }
+
+                       boolean valid(Text[] args) {
+                               return args.length > 0 && args[0].type() == Text.Type.IDENTIFIER;
+                       }
+
+                       boolean validKey(Text[] args) {
+                               return args.length == 1 && args[0].type() == Text.Type.IDENTIFIER;
+                       }
+
+                       boolean validCompare(Text[] args) {
+                               // {.key val ifeq ifne}
+                               return args.length >= 2 && args.length <= 4 && args[0].type() == Text.Type.IDENTIFIER;
+                       }
+
+                       boolean validBoolean(Text[] args) {
+                               // {.key ift iff}
+                               return args.length >= 2 && args.length <= 3 && args[0].type() == Text.Type.IDENTIFIER;
+                       }
+
+               }
+       }
+
+       static class TemplateTokeniser extends Tokeniser {
+               private int state = 0;
+               Command.Type cmd;
+
+               public TemplateTokeniser(Reader src) {
+                       super(src);
+               }
+
+               static final int TOK_COMMAND = Tokeniser.TOK_LAST - 1;
+               static final char START = '{';
+               static final char FINISH = '}';
+
+               Command.Type getCommand() {
+                       return cmd;
+               }
+
+               public int nextToken() throws IOException {
+                       switch (state) {
+                       case 0 -> {
+                               int c, last = -1;
+
+                               text.setLength(0);
+                               textDrop = 0;
+
+                               while ((c = read()) != -1) {
+                                       if (last == START && (cmd = Command.Type.valueOf((char)c)) != Command.Type.LITERAL) {
+                                               state = 1;
+                                               textDrop = 1;
+                                               return Tokeniser.TOK_LITERAL;
+                                       } else {
+                                               text.append((char)c);
+                                               // if by line
+                                               if (c == '\n')
+                                                       return Tokeniser.TOK_LITERAL;
+                                       }
+                                       last = c;
+                               }
+                               if (text.isEmpty())
+                                       return c;
+                               else
+                                       return Tokeniser.TOK_LITERAL;
+                       }
+                       case 1 -> {
+                               state = 2;
+                               return TOK_COMMAND;
+                       }
+                       case 2 -> {
+                               int c = super.nextToken();
+
+                               if (c == FINISH)
+                                       state = 0;
+                               return c;
+                       }
+                       default ->
+                               throw new AssertionError(state);
+                       }
+               }
+       }
+
+       static class TemplateParser implements AutoCloseable {
+               private final Path base;
+               private final TemplateTokeniser in;
+
+               public TemplateParser(Path base, TemplateTokeniser in) {
+                       this.base = base;
+                       this.in = in;
+               }
+
+               public TemplateParser(TemplateTokeniser in) {
+                       this.base = null;
+                       this.in = in;
+               }
+
+               @Override
+               public void close() throws IOException {
+                       in.close();
+               }
+
+               Command newCommand(Command.Type type, List<Text> values) throws IOException {
+                       Text[] array = values.toArray(Text[]::new);
+                       if (!type.valid(array))
+                               throw new IOException(in.fatalMessage(0, "Command arguments invalid for: " + type));
+                       return new Command(type, array);
+               }
+
+               Template load() throws IOException {
+                       int t;
+                       List<Command> seq = new ArrayList<>();
+                       List<Text> texts = new ArrayList<>();
+
+                       // would be nice if it could strip any common prefix
+                       // from the input(e.g. from the first line)
+                       while ((t = in.nextToken()) != -1) {
+                               switch (t) {
+                               case Tokeniser.TOK_LITERAL -> {
+                                       texts.add(new Text(Text.Type.LITERAL, in.getText()));
+                               }
+                               case TemplateTokeniser.TOK_COMMAND -> {
+                                       Command.Type type = in.getCommand();
+                                       if (!texts.isEmpty()) {
+                                               seq.add(newCommand(Command.Type.LITERAL, texts));
+                                               texts.clear();
+                                       }
+
+                                       while ((t = in.nextToken()) != -1 && t != '}') {
+                                               texts.add(in.getText(t));
+                                       }
+                                       seq.add(newCommand(type, texts));
+                                       texts.clear();
+                               }
+                               default -> {
+                                       throw new IOException(in.fatalMessage(t, "Invalid"));
+                               }
+                               }
+                       }
+                       if (!texts.isEmpty())
+                               seq.add(newCommand(Command.Type.LITERAL, texts));
+                       return new Template(seq);
+               }
+       }
+
+       public static void main(String[] args) throws IOException {
+               Template t = load(Path.of("test.tmp"));
+
+               t.seq.forEach(c -> {
+                       System.out.println(c.type);
+                       for (var v: c.values)
+                               System.out.printf("\t%s\n", v);
+               });
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java
new file mode 100644 (file)
index 0000000..64c0685
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+/**
+ *
+ */
+public record Text(Type type, String value) {
+
+       public static final Text UNDEFINED = new Text(Type.UNDEFINED, "");
+       public static final Text TRUE = new Text(Type.LITERAL, "1");
+       public static final Text FALSE = new Text(Type.LITERAL, "0");
+
+       public enum Type {
+               IDENTIFIER,
+               STRING,
+               LITERAL,
+               PATTERN,
+               UNDEFINED;
+       }
+
+       public boolean defined() {
+               return this != UNDEFINED;
+       }
+
+       public Value toValue() {
+               if (defined())
+                       return new Scalar(value);
+               else
+                       return Scalar.UNDEFINED;
+       }
+
+       public static Text ofValue(Value value) {
+               if (value.defined() && value instanceof Scalar s)
+                       return ofLiteral(s.getValue());
+               else
+                       return UNDEFINED;
+       }
+
+       public static Text ofIdentifier(String value) {
+               return new Text(Type.IDENTIFIER, value);
+       }
+
+       public static Text ofString(String value) {
+               return new Text(Type.STRING, value);
+       }
+
+       public static Text ofLiteral(String value) {
+               return new Text(Type.LITERAL, value);
+       }
+
+       public static Text ofPattern(String value) {
+               return new Text(Type.PATTERN, value);
+       }
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Tokeniser.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Tokeniser.java
new file mode 100644 (file)
index 0000000..d0ce00e
--- /dev/null
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.foreign.MemorySegment;
+import java.nio.file.Path;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Tokeniser for a few file formats.
+ * <p>
+ * <ul>
+ * <li>'...' string, escapes \[rnt] or \.
+ * <li>"..." string, escapes \[rnt] or \.
+ * <li>/.../ regex string, no escapes
+ * <li>@...@ regex string, no escapes
+ * <li>&lt;[ ... ]&gt; literal string (nested - maybe it shouldn't be)
+ * <li>&lt;{ ... }&gt; string (nested)
+ * <li>[::identifier::]+ identifier
+ * <li>[.] character
+ * </ul>
+ */
+public class Tokeniser implements AutoCloseable {
+       // Really want paths as well?
+       List<Reader> readers = new LinkedList<>();
+       final int commentChar = '#';
+       Reader src;
+       int lastc = -1;
+       StringBuilder text = new StringBuilder();
+       int textDrop = 0;
+       // For diagnostics only
+       StringBuilder line = new StringBuilder();
+       int lineNo = 1;
+
+       // Not linked up yet: for diagnostics mostly
+       static class Source {
+               Reader src;
+               Path file; // if known
+               int lineNo = 1;
+               StringBuilder line = new StringBuilder();
+
+               public Source(String txt) {
+               }
+
+               public Source(Path file) {
+                       this.src = src;
+                       this.file = file;
+               }
+       }
+
+       public Tokeniser(Reader src) {
+               this.src = src;
+               readers.addFirst(src);
+       }
+
+       @Override
+       public void close() throws IOException {
+               readers.forEach(r -> {
+                       try {
+                               r.close();
+                       } catch (IOException ex) {
+                       }
+               });
+       }
+
+       Supplier<String> logMessage(String why) {
+               int row = lineNo;
+               int col = line.length();
+               StringBuilder marker = new StringBuilder();
+               StringBuilder line = new StringBuilder(this.line);
+
+               try {
+                       int c;
+                       while ((c = readInternal()) != -1 && c != '\n')
+                               line.append((char)c);
+                       for (int i = 0; i < col - 1; i++) {
+                               if (line.charAt(i) == '\t')
+                                       marker.append("\t");
+                               else
+                                       marker.append(i >= col - 5 ? "~" : " ");
+                       }
+                       marker.append("^");
+               } catch (IOException x) {
+               }
+
+               return () -> String.format("template:%d:%d: %s\n %5d | %s\n %5d | %s\n", row, col, why, row, line, row, marker);
+       }
+
+       String fatalMessage(String got, String why) {
+
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, logMessage(why));
+
+               // TODO: get filename
+               //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format("template:%d:%d: %s%s", row, col, why, got));
+               //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format(" %5d | %s\n", row, line));
+               //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format(" %5d | %s\n", row, marker));
+               return (why + " got '" + got + "' (" + lineNo + "): " + line);
+       }
+
+       String fatalMessage(String why) {
+               return fatalMessage("", why);
+       }
+
+       String fatalMessage(int t, String why) {
+               String got = switch (t) {
+                       case TOK_EOF ->
+                               "EOF";
+                       case TOK_IDENTIFIER ->
+                               "IDENTIFIER";
+                       case TOK_REGEX ->
+                               "/REGEX/";
+                       case TOK_STRING_SQ ->
+                               "'STRING'";
+                       case TOK_STRING_DQ ->
+                               "\"STRING\"";
+                       case TOK_QUOTED ->
+                               "<{ QUOTED }>";
+                       case TOK_LITERAL ->
+                               "<[ LITERAL ]>";
+                       default ->
+                               new String(new char[]{(char)t});
+               };
+               return fatalMessage(got, why);
+       }
+
+       void push(Reader r) {
+               readers.addFirst(r);
+               this.src = r;
+       }
+
+       void push(String string) {
+               if (string.length() > 0)
+                       push(Reader.of(string));
+       }
+
+       void push(int c) {
+               assert (lastc == -1);
+               lastc = c;
+       }
+
+       /**
+        * Read but without line processing.
+        *
+        * @return
+        * @throws IOException
+        */
+       int readInternal() throws IOException {
+               int c = -1;
+
+               if (lastc != -1) {
+                       c = lastc;
+                       lastc = -1;
+                       return c;
+               }
+
+               while (!readers.isEmpty() && (c = src.read()) == -1) {
+                       readers.removeFirst();
+                       src = readers.isEmpty() ? null : readers.getFirst();
+               }
+
+               return c;
+       }
+
+       /**
+        * Read next char.
+        *
+        * @return
+        * @throws IOException
+        */
+       int read() throws IOException {
+               int c = readInternal();
+
+               if (c == '\n') {
+                       line.setLength(0);
+                       lineNo += 1;
+               } else if (c != -1)
+                       line.append((char)c);
+
+               return c;
+       }
+
+       int skipLWS() throws IOException {
+               int c;
+
+               do {
+                       c = read();
+
+                       if (c == commentChar) {
+                               int q;
+                               do {
+                                       q = read();
+                               } while (q != -1 && q != '\n');
+                       }
+               } while (c == commentChar || Character.isWhitespace(c));
+
+               return c;
+       }
+
+       // identifier characters bitmap
+       static final long ida = 0x87ffe85200000000L;
+       static final long idb = 0x07fffffe87ffffffL;
+
+       /*
+       static{
+               long a = 0, b = 0;
+               for (int i = 0; i < 64; i++) {
+                       int j = i + 64;
+                       if ("!$&+-./:?".indexOf((char)i) >= 0
+                               || (i >= '0' && i <= '9'))
+                               a |= 1L << i;
+                       if ("@_".indexOf((char)(j)) >= 0
+                               || (j >= 'a' && j <= 'z')
+                               || (j >= 'A' && j <= 'Z'))
+                               b |= 1L << i;
+               }
+               //ida = a;
+               //idb = b;
+               System.out.printf("     static final long ida = 0x%016xL;\n", a);
+               System.out.printf("     static final long idb = 0x%016xL;\n", b);
+       }*/
+
+       boolean isIdentifier(int c) {
+               return c < 64
+                       ? (ida & (1L << c)) != 0
+                       : (idb & (1L << c - 64)) != 0;
+       }
+
+       /*
+        * Maybe '' should also be a literal string (not parsed) like perl.
+        */
+       static final int TOK_EOF = -1;
+       static final int TOK_IDENTIFIER = -2;
+       static final int TOK_STRING_SQ = -3;
+       static final int TOK_STRING_DQ = -4;
+       static final int TOK_REGEX = -5;
+       /**
+        * &lt;{ quoted }&gt;
+        */
+       static final int TOK_QUOTED = -6;
+       /**
+        * &lt;[ quoted ]&gt;
+        */
+       static final int TOK_LITERAL = -7;
+       static final int TOK_LAST = -7;
+
+       int literal(int a, int b) throws IOException {
+               int c, last = -1;
+               int depth = 1;
+
+               while ((c = read()) != -1) {
+                       if (last == b && c == '>') {
+                               depth--;
+                               if (depth == 0)
+                                       break;
+                       } else if (last == '<' && c == a) {
+                               depth += 1;
+                       }
+                       text.append((char)c);
+                       last = c;
+               }
+
+               if (depth != 0)
+                       throw new IOException(fatalMessage(c, "Truncated <" + a + " quoted " + b + ">"));
+
+               textDrop = (c == -1 ? 0 : 1);
+
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token literal quoted: " + text.toString());
+
+               return b == ']' ? TOK_LITERAL : TOK_QUOTED;
+       }
+
+       int regex(int q) throws IOException {
+               int c;
+               while ((c = read()) != -1 && c != q) {
+                       text.append((char)c);
+               }
+               if (c != q)
+                       throw new IOException(fatalMessage(c, "Truncated " + q + "regex" + q));
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token regex: " + text.toString());
+               return TOK_REGEX;
+       }
+
+       String regex() throws IOException {
+               int c = nextToken();
+               if (c == TOK_REGEX)
+                       return getText();
+               else
+                       throw new IOException(fatalMessage(c, "Expecting /regex/ or @regex@"));
+       }
+
+       int quoted(int q) throws IOException {
+               int c;
+               while ((c = read()) != -1 && c != q) {
+                       if (c == '\\') {
+                               c = read();
+                               c = switch (c) {
+                                       case 'n' ->
+                                               '\n';
+                                       case 'r' ->
+                                               '\r';
+                                       case 't' ->
+                                               '\t';
+                                       case -1 ->
+                                               '\\';
+                                       default ->
+                                               c;
+                               };
+                       }
+
+                       text.append((char)c);
+               }
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token quoted: " + text.toString());
+               return q == '"' ? TOK_STRING_DQ : TOK_STRING_SQ;
+       }
+
+       public String getText() {
+               return textDrop == 0
+                       ? text.toString()
+                       : text.substring(0, text.length() - textDrop);
+       }
+
+       public Text getText(int t) throws IOException {
+               Text.Type type = switch (t) {
+                       case Tokeniser.TOK_IDENTIFIER ->
+                               Text.Type.IDENTIFIER;
+                       case Tokeniser.TOK_STRING_DQ, Tokeniser.TOK_QUOTED ->
+                               Text.Type.STRING;
+                       case Tokeniser.TOK_STRING_SQ, Tokeniser.TOK_LITERAL ->
+                               Text.Type.LITERAL;
+                       case Tokeniser.TOK_REGEX ->
+                               Text.Type.PATTERN;
+                       default ->
+                               throw new IOException(fatalMessage(t, "Expected string or ident"));
+               };
+
+               return new Text(type, getText());
+       }
+
+       // Parse a specific token only
+       public void nextToken(char[] seq) throws IOException {
+               int c = skipLWS();
+               int i = 0;
+
+               while (i < seq.length && seq[i] == c) {
+                       i++;
+                       c = read();
+               }
+               if (i != seq.length)
+                       throw new IOException(fatalMessage(c, "Expected:" + new String(seq)));
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token seq: " + new String(seq));
+       }
+
+       public void nextToken(char c) throws IOException {
+               int t;
+               if ((t = skipLWS()) != c)
+                       throw new IOException(fatalMessage(t, "Expected:" + c));
+               System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)c);
+       }
+
+       public int nextToken() throws IOException {
+               int c = skipLWS();
+
+               text.setLength(0);
+               textDrop = 0;
+
+               if (c == -1) {
+                       return c;
+               } else if (c == '\'' || c == '"') {
+                       return quoted(c);
+               } else if (c == '/' || c == '@') {
+                       return regex(c);
+               } else if (c == '<') {
+                       c = read();
+                       if (c == '{')
+                               return literal('{', '}');
+                       else if (c == '[')
+                               return literal('[', ']');
+                       else
+                               throw new IOException("Not valid literal, must be <[]> or <{}>");
+               } else if (isIdentifier(c)) {
+                       do {
+                               text.append((char)c);
+                       } while ((c = read()) != -1 && isIdentifier(c));
+                       if (c != -1)
+                               push(c);
+                       System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token ident: " + text.toString());
+                       return TOK_IDENTIFIER;
+               } else {
+                       int l = c;
+                       System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)l);
+                       return c;
+               }
+       }
+
+       /*
+       public static void main(String[] args) throws IOException {
+               if (false) {
+                       long a = 0, b = 0;
+                       for (int i = 0; i < 64; i++) {
+                               int j = i + 64;
+                               if ("!$&+-./:?".indexOf((char)i) >= 0
+                                       || (i >= '0' && i <= '9'))
+                                       a |= 1L << i;
+                               if ("@_".indexOf((char)(j)) >= 0
+                                       || (j >= 'a' && j <= 'z')
+                                       || (j >= 'A' && j <= 'Z'))
+                                       b |= 1L << i;
+                       }
+                       System.out.printf(" %016x %016x\n", a, b);
+                       return;
+               }
+
+               try (FileReader r = new FileReader("test.api")) {
+                       int t;
+
+                       Tokeniser tok = new Tokeniser(r);
+                       while ((t = tok.nextToken()) != tok.TOK_EOF) {
+                               System.out.printf("tok: %d %c '%s'\n", t, t >= 0 ? t : '.', tok.text());
+                       }
+               }
+
+       }*/
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Util.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Util.java
new file mode 100644 (file)
index 0000000..05ef8a7
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.util.function.Function;
+
+public class Util {
+
+       static String capify(String name, boolean cap) {
+               char[] string = name.toCharArray();
+               int j = 0;
+
+               for (int i = 0; i < string.length; i++) {
+                       if (string[i] == '_') {
+                               if (j > 0)
+                                       cap = true;
+                       } else if (!cap) {
+                               string[j++] = string[i];
+                       } else {
+                               string[j++] = Character.toUpperCase(string[i]);
+                               cap = false;
+                       }
+               }
+               return new String(string, 0, j);
+       }
+
+       static String toStudlyCaps(String name) {
+               return capify(name, true);
+       }
+
+       static String toCamelCase(String name) {
+               return capify(name, false);
+       }
+
+       public static void main(String[] args) {
+               String[] names = {
+                       "do_thing_blah",
+                       "a_b",
+                       "_foo_bar",
+                       "foobar"
+               };
+               String[] studly = {
+                       "DoThingBlah",
+                       "AB",
+                       "FooBar",
+                       "Foobar"
+               };
+               String[] camel = {
+                       "doThingBlah",
+                       "aB",
+                       "fooBar",
+                       "foobar"
+               };
+
+               for (int i = 0; i < names.length; i++) {
+                       String s = names[i];
+                       String a = toStudlyCaps(s);
+                       String b = toCamelCase(s);
+
+                       System.out.printf("%12s %12s %12s\n", s, a, b);
+                       assert (a.equals(studly[i]));
+                       assert (b.equals(camel[i]));
+               }
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Value.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Value.java
new file mode 100644 (file)
index 0000000..87d8ef8
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public abstract class Value {
+
+       abstract void dumper(StringBuilder sb, String indent);
+
+       abstract boolean defined();
+
+       String dumper() {
+               StringBuilder sb = new StringBuilder();
+               dumper(sb, "");
+               return sb.toString();
+       }
+
+       void dump(PrintStream out) {
+               StringBuilder sb = new StringBuilder();
+               dumper(sb, "");
+               out.print(sb);
+       }
+
+       static class ValueParser implements AutoCloseable {
+               static final char[] assoc = {'=', '>'};
+               private final Tokeniser in;
+
+               public ValueParser(Tokeniser in) {
+                       this.in = in;
+               }
+
+               @Override
+               public void close() throws IOException {
+                       in.close();
+               }
+
+               Value readValue() throws IOException {
+                       return readValue(in.nextToken());
+               }
+
+               Value readValue(int t) throws IOException {
+                       switch (t) {
+                       case Tokeniser.TOK_STRING_DQ:
+                       case Tokeniser.TOK_STRING_SQ:
+                       case Tokeniser.TOK_IDENTIFIER:
+                               return new Scalar(in.getText());
+                       case '{':
+                               Hash hash = new Hash();
+                               while ((t = in.nextToken()) != -1 && t != '}') {
+                                       if (t != Tokeniser.TOK_IDENTIFIER && t != Tokeniser.TOK_STRING_SQ && t != Tokeniser.TOK_STRING_DQ)
+                                               throw new IOException(in.fatalMessage(t, "Expecting string or name"));
+
+                                       String key = in.getText();
+                                       Value val;
+
+                                       in.nextToken(assoc);
+                                       t = in.nextToken();
+                                       val = readValue(t);
+                                       hash.hash.put(key, val);
+                                       t = in.nextToken();
+                                       if (t == '}')
+                                               break;
+                                       else if (t != ',')
+                                               throw new IOException("Expecting '}' or ','");
+                               }
+                               return hash;
+                       case '[':
+                               Array array = new Array();
+                               while ((t = in.nextToken()) != -1 && t != ']') {
+                                       array.array.add(readValue(t));
+                                       t = in.nextToken();
+                                       if (t == ']')
+                                               break;
+                                       else if (t != ',')
+                                               throw new IOException("Expecting '}' or ','");
+                               }
+                               return array;
+                       default:
+                               throw new IOException(in.fatalMessage(t, "Unexpected token"));
+                       }
+               }
+       }
+
+       static Value load(Path src) throws IOException {
+               try (ValueParser p = new ValueParser(new Tokeniser(Files.newBufferedReader(src)))) {
+                       return p.readValue();
+               }
+       }
+
+}
diff --git a/src/notzed.nativez.tools/classes/module-info.java b/src/notzed.nativez.tools/classes/module-info.java
new file mode 100644 (file)
index 0000000..23e10ed
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+module notzed.nativez.tools {
+}