--- /dev/null
+/*
+ * 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"));
+ }
+
+}
--- /dev/null
+/*
+ * 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("]");
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ */
+ }
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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"));
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ });
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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><[ ... ]> literal string (nested - maybe it shouldn't be)
+ * <li><{ ... }> 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;
+ /**
+ * <{ quoted }>
+ */
+ static final int TOK_QUOTED = -6;
+ /**
+ * <[ quoted ]>
+ */
+ 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());
+ }
+ }
+
+ }*/
+}
--- /dev/null
+/*
+ * 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]));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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 {
+}